From 2aaa6371abc8a240692ce1468591746abf6d1a12 Mon Sep 17 00:00:00 2001 From: hstyi Date: Sun, 20 Jul 2025 12:15:53 +0800 Subject: [PATCH] feat: support VNC protocol --- plugins/LICENSE | 3 +- plugins/vnc/build.gradle.kts | 17 + .../libs/trilead-ssh2-build217-jenkins-8.jar | Bin 0 -> 262523 bytes .../glavsoft/core/SettingsChangedEvent.java | 40 + .../com/glavsoft/drawing/ColorDecoder.java | 144 +++ .../java/com/glavsoft/drawing/Renderer.java | 340 ++++++ .../java/com/glavsoft/drawing/SoftCursor.java | 91 ++ .../AuthenticationFailedException.java | 45 + .../exceptions/ClosedConnectionException.java | 36 + .../glavsoft/exceptions/CommonException.java | 39 + .../exceptions/CouldNotConnectException.java | 33 + .../glavsoft/exceptions/CryptoException.java | 34 + .../glavsoft/exceptions/FatalException.java | 34 + .../exceptions/ProtocolException.java | 31 + .../exceptions/TransportException.java | 39 + .../UnsupportedProtocolVersionException.java | 32 + .../UnsupportedSecurityTypeException.java | 32 + .../com/glavsoft/rfb/ClipboardController.java | 58 + .../glavsoft/rfb/IChangeSettingsListener.java | 33 + .../com/glavsoft/rfb/IRepaintController.java | 41 + .../java/com/glavsoft/rfb/IRequestString.java | 28 + .../com/glavsoft/rfb/IRfbSessionListener.java | 38 + .../com/glavsoft/rfb/RfbCapabilityInfo.java | 135 +++ .../rfb/client/ClientCutTextMessage.java | 66 ++ .../rfb/client/ClientMessageType.java | 66 ++ .../rfb/client/ClientToServerMessage.java | 31 + .../FramebufferUpdateRequestMessage.java | 64 ++ .../glavsoft/rfb/client/KeyEventMessage.java | 65 ++ .../rfb/client/PointerEventMessage.java | 55 + .../rfb/client/SetEncodingsMessage.java | 60 ++ .../rfb/client/SetPixelFormatMessage.java | 45 + .../rfb/client/VideoFreezeMessage.java | 46 + .../VideoRectangleSelectionMessage.java | 56 + .../glavsoft/rfb/encoding/EncodingType.java | 156 +++ .../glavsoft/rfb/encoding/PixelFormat.java | 185 ++++ .../rfb/encoding/ServerInitMessage.java | 77 ++ .../rfb/encoding/decoder/ByteBuffer.java | 65 ++ .../rfb/encoding/decoder/CopyRectDecoder.java | 41 + .../encoding/decoder/CursorPosDecoder.java | 39 + .../rfb/encoding/decoder/Decoder.java | 43 + .../encoding/decoder/DesctopSizeDecoder.java | 39 + .../rfb/encoding/decoder/FakeDecoder.java | 39 + .../decoder/FramebufferUpdateRectangle.java | 76 ++ .../rfb/encoding/decoder/HextileDecoder.java | 103 ++ .../rfb/encoding/decoder/RREDecoder.java | 49 + .../rfb/encoding/decoder/RawDecoder.java | 51 + .../encoding/decoder/RichCursorDecoder.java | 73 ++ .../rfb/encoding/decoder/TightDecoder.java | 291 +++++ .../rfb/encoding/decoder/ZRLEDecoder.java | 166 +++ .../rfb/encoding/decoder/ZlibDecoder.java | 71 ++ .../glavsoft/rfb/protocol/LocalPointer.java | 33 + .../glavsoft/rfb/protocol/MessageQueue.java | 60 ++ .../com/glavsoft/rfb/protocol/Protocol.java | 473 +++++++++ .../rfb/protocol/ProtocolContext.java | 105 ++ .../rfb/protocol/ProtocolSettings.java | 343 ++++++ .../glavsoft/rfb/protocol/ReceiverTask.java | 204 ++++ .../com/glavsoft/rfb/protocol/SenderTask.java | 79 ++ .../rfb/protocol/auth/AuthHandler.java | 117 +++ .../rfb/protocol/auth/NoneAuthentication.java | 42 + .../rfb/protocol/auth/SecurityType.java | 49 + .../protocol/auth/TightAuthentication.java | 268 +++++ .../rfb/protocol/auth/VncAuthentication.java | 103 ++ .../rfb/protocol/handlers/Handshaker.java | 345 ++++++ .../rfb/protocol/tunnel/SslTunnel.java | 121 +++ .../rfb/protocol/tunnel/TunnelHandler.java | 36 + .../rfb/protocol/tunnel/TunnelType.java | 55 + .../com/glavsoft/transport/BaudrateMeter.java | 65 ++ .../com/glavsoft/transport/Transport.java | 349 ++++++ .../main/java/com/glavsoft/utils/Keymap.java | 904 ++++++++++++++++ .../java/com/glavsoft/utils/LazyLoaded.java | 63 ++ .../main/java/com/glavsoft/utils/Strings.java | 62 ++ .../com/glavsoft/utils/ViewerControlApi.java | 86 ++ .../java/com/glavsoft/viewer/cli/Parser.java | 119 +++ .../java/com/glavsoft/viewer/mvp/Model.java | 37 + .../com/glavsoft/viewer/mvp/Presenter.java | 281 +++++ .../viewer/mvp/PropertyNotFoundException.java | 35 + .../java/com/glavsoft/viewer/mvp/View.java | 45 + .../viewer/settings/ConnectionParams.java | 284 +++++ .../settings/LocalMouseCursorShape.java | 44 + .../glavsoft/viewer/settings/UiSettings.java | 189 ++++ .../viewer/settings/UiSettingsData.java | 99 ++ .../settings/WrongParameterException.java | 47 + .../swing/CancelConnectionException.java | 36 + .../CancelConnectionQuietlyException.java | 34 + .../viewer/swing/ClipboardControllerImpl.java | 152 +++ .../swing/ConnectionErrorException.java | 36 + .../viewer/swing/ConnectionPresenter.java | 370 +++++++ .../viewer/swing/KeyEventListener.java | 268 +++++ .../viewer/swing/KeyboardConvertor.java | 150 +++ .../swing/ModifierButtonEventListener.java | 42 + .../viewer/swing/MouseEnteredListener.java | 33 + .../viewer/swing/MouseEventListener.java | 123 +++ .../glavsoft/viewer/swing/RendererImpl.java | 138 +++ .../glavsoft/viewer/swing/SoftCursorImpl.java | 50 + .../com/glavsoft/viewer/swing/Surface.java | 272 +++++ .../swing/SwingConnectionWorkerFactory.java | 69 ++ .../swing/SwingNetworkConnectionWorker.java | 199 ++++ .../swing/SwingRfbConnectionWorker.java | 313 ++++++ .../viewer/swing/SwingViewerWindow.java | 993 ++++++++++++++++++ .../swing/SwingViewerWindowFactory.java | 118 +++ .../java/com/glavsoft/viewer/swing/Utils.java | 141 +++ .../viewer/swing/ViewerEventsListener.java | 41 + .../AutoCompletionComboEditorDocument.java | 136 +++ .../swing/gui/ConnectionDialogView.java | 497 +++++++++ .../viewer/swing/gui/ConnectionInfoView.java | 188 ++++ .../viewer/swing/gui/ConnectionView.java | 41 + .../viewer/swing/gui/ConnectionsHistory.java | 368 +++++++ .../swing/gui/HostnameComboboxRenderer.java | 60 ++ .../viewer/swing/gui/OptionsDialog.java | 520 +++++++++ .../swing/gui/RequestSomethingDialog.java | 191 ++++ .../swing/mac/MacApplicationWrapper.java | 102 ++ .../glavsoft/viewer/swing/mac/MacUtils.java | 62 ++ .../viewer/swing/ssh/PrefsHelper.java | 95 ++ .../viewer/swing/ssh/RequestYesNoDialog.java | 64 ++ .../swing/ssh/SshConnectionManager.java | 165 +++ .../ssh/TrileadSsh2ConnectionManager.java | 313 ++++++ .../AbstractConnectionWorkerFactory.java | 44 + .../viewer/workers/ConnectionWorker.java | 46 + .../workers/NetworkConnectionWorker.java | 41 + .../viewer/workers/RfbConnectionWorker.java | 46 + .../kotlin/app/termora/plugins/vnc/MyIcons.kt | 39 + .../termora/plugins/vnc/VNCHostOptionsPane.kt | 279 +++++ .../app/termora/plugins/vnc/VNCPlugin.kt | 28 + .../plugins/vnc/VNCProtocolHostPanel.kt | 36 + .../vnc/VNCProtocolHostPanelExtension.kt | 22 + .../plugins/vnc/VNCProtocolProvider.kt | 34 + .../vnc/VNCProtocolProviderExtension.kt | 14 + .../app/termora/plugins/vnc/VNCTerminalTab.kt | 28 + .../app/termora/plugins/vnc/VNCViewer.kt | 263 +++++ .../src/main/resources/META-INF/plugin.xml | 22 + .../main/resources/META-INF/pluginIcon.svg | 1 + .../resources/META-INF/pluginIcon_dark.svg | 1 + .../com/glavsoft/viewer/images/button-alt.png | Bin 0 -> 263 bytes .../glavsoft/viewer/images/button-close.png | Bin 0 -> 214 bytes .../viewer/images/button-ctrl-alt-del.png | Bin 0 -> 319 bytes .../glavsoft/viewer/images/button-ctrl.png | Bin 0 -> 269 bytes .../viewer/images/button-file-transfer.png | Bin 0 -> 316 bytes .../glavsoft/viewer/images/button-info.png | Bin 0 -> 289 bytes .../viewer/images/button-new-connection.png | Bin 0 -> 305 bytes .../glavsoft/viewer/images/button-options.png | Bin 0 -> 299 bytes .../com/glavsoft/viewer/images/button-rec.png | Bin 0 -> 267 bytes .../glavsoft/viewer/images/button-refresh.png | Bin 0 -> 293 bytes .../glavsoft/viewer/images/button-save.png | Bin 0 -> 274 bytes .../com/glavsoft/viewer/images/button-win.png | Bin 0 -> 328 bytes .../viewer/images/button-zoom-100.png | Bin 0 -> 307 bytes .../viewer/images/button-zoom-fit.png | Bin 0 -> 340 bytes .../viewer/images/button-zoom-fullscreen.png | Bin 0 -> 298 bytes .../glavsoft/viewer/images/button-zoom-in.png | Bin 0 -> 292 bytes .../viewer/images/button-zoom-out.png | Bin 0 -> 292 bytes .../com/glavsoft/viewer/images/cursor-dot.png | Bin 0 -> 168 bytes .../viewer/images/cursor-nocursor.png | Bin 0 -> 148 bytes .../viewer/images/cursor-smalldot.png | Bin 0 -> 183 bytes .../viewer/images/tightvnc-logo-128x128.png | Bin 0 -> 13783 bytes .../viewer/images/tightvnc-logo-16x16.png | Bin 0 -> 694 bytes .../viewer/images/tightvnc-logo-32x32.png | Bin 0 -> 1388 bytes .../viewer/images/tightvnc-logo-48x48.png | Bin 0 -> 2704 bytes .../src/main/resources/icons/actualZoom.svg | 4 + .../main/resources/icons/actualZoom_dark.svg | 4 + .../src/main/resources/icons/ctrlAltDel.svg | 5 + .../main/resources/icons/ctrlAltDel_dark.svg | 5 + .../vnc/src/main/resources/icons/zoomIn.svg | 6 + .../src/main/resources/icons/zoomIn_dark.svg | 6 + .../vnc/src/main/resources/icons/zoomOut.svg | 5 + .../src/main/resources/icons/zoomOut_dark.svg | 5 + .../app/termora/plugins/vnc/MySurface.kt | 9 + .../app/termora/plugins/vnc/RfbClientTest.kt | 62 ++ settings.gradle.kts | 1 + src/main/kotlin/app/termora/DynamicIcon.kt | 18 +- 168 files changed, 16033 insertions(+), 4 deletions(-) create mode 100644 plugins/vnc/build.gradle.kts create mode 100644 plugins/vnc/libs/trilead-ssh2-build217-jenkins-8.jar create mode 100644 plugins/vnc/src/main/java/com/glavsoft/core/SettingsChangedEvent.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/drawing/ColorDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/drawing/Renderer.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/drawing/SoftCursor.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/AuthenticationFailedException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/ClosedConnectionException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/CommonException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/CouldNotConnectException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/CryptoException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/FatalException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/ProtocolException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/TransportException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedProtocolVersionException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedSecurityTypeException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/ClipboardController.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/IChangeSettingsListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/IRepaintController.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/IRequestString.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/IRfbSessionListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/RfbCapabilityInfo.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientCutTextMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientMessageType.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientToServerMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/FramebufferUpdateRequestMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/KeyEventMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/PointerEventMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetEncodingsMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetPixelFormatMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoFreezeMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoRectangleSelectionMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/EncodingType.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/PixelFormat.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/ServerInitMessage.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ByteBuffer.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CopyRectDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CursorPosDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/Decoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/DesctopSizeDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FakeDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FramebufferUpdateRectangle.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/HextileDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RREDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RawDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RichCursorDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/TightDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ZRLEDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ZlibDecoder.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/LocalPointer.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/MessageQueue.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/Protocol.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolContext.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolSettings.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ReceiverTask.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/SenderTask.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/AuthHandler.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/NoneAuthentication.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/SecurityType.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/TightAuthentication.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/VncAuthentication.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/handlers/Handshaker.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/SslTunnel.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelHandler.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelType.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/transport/BaudrateMeter.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/transport/Transport.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/utils/Keymap.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/utils/LazyLoaded.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/utils/Strings.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/utils/ViewerControlApi.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/cli/Parser.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/Model.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/Presenter.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/PropertyNotFoundException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/View.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/settings/ConnectionParams.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/settings/LocalMouseCursorShape.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettings.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettingsData.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/settings/WrongParameterException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionQuietlyException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ClipboardControllerImpl.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionErrorException.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionPresenter.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyEventListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyboardConvertor.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ModifierButtonEventListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEnteredListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEventListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/RendererImpl.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SoftCursorImpl.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Surface.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingConnectionWorkerFactory.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingNetworkConnectionWorker.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingRfbConnectionWorker.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindow.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindowFactory.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Utils.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ViewerEventsListener.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/AutoCompletionComboEditorDocument.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionDialogView.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionInfoView.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionView.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionsHistory.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/HostnameComboboxRenderer.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/OptionsDialog.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/RequestSomethingDialog.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacApplicationWrapper.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacUtils.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/PrefsHelper.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/RequestYesNoDialog.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/SshConnectionManager.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/TrileadSsh2ConnectionManager.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/workers/AbstractConnectionWorkerFactory.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/workers/ConnectionWorker.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/workers/NetworkConnectionWorker.java create mode 100644 plugins/vnc/src/main/java/com/glavsoft/viewer/workers/RfbConnectionWorker.java create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/MyIcons.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCHostOptionsPane.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCPlugin.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanel.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanelExtension.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProvider.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProviderExtension.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCTerminalTab.kt create mode 100644 plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCViewer.kt create mode 100644 plugins/vnc/src/main/resources/META-INF/plugin.xml create mode 100644 plugins/vnc/src/main/resources/META-INF/pluginIcon.svg create mode 100644 plugins/vnc/src/main/resources/META-INF/pluginIcon_dark.svg create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-alt.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-close.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-ctrl-alt-del.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-ctrl.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-file-transfer.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-info.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-new-connection.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-options.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-rec.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-refresh.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-save.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-win.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-100.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-fit.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-fullscreen.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-in.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-out.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/cursor-dot.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/cursor-nocursor.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/cursor-smalldot.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-128x128.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-16x16.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-32x32.png create mode 100644 plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-48x48.png create mode 100644 plugins/vnc/src/main/resources/icons/actualZoom.svg create mode 100644 plugins/vnc/src/main/resources/icons/actualZoom_dark.svg create mode 100644 plugins/vnc/src/main/resources/icons/ctrlAltDel.svg create mode 100644 plugins/vnc/src/main/resources/icons/ctrlAltDel_dark.svg create mode 100644 plugins/vnc/src/main/resources/icons/zoomIn.svg create mode 100644 plugins/vnc/src/main/resources/icons/zoomIn_dark.svg create mode 100644 plugins/vnc/src/main/resources/icons/zoomOut.svg create mode 100644 plugins/vnc/src/main/resources/icons/zoomOut_dark.svg create mode 100644 plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/MySurface.kt create mode 100644 plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/RfbClientTest.kt diff --git a/plugins/LICENSE b/plugins/LICENSE index 285ddb3..2442439 100644 --- a/plugins/LICENSE +++ b/plugins/LICENSE @@ -5,6 +5,7 @@ The files in this catalogue are for public access only. Specific descriptions ar - You may view and study the contents of these files; - You may NOT use them for any commercial purpose; - You may NOT modify, copy, distribute, republish, or use them to create derivative works; -- Written permission must be obtained from the author for any use beyond personal viewing. +- Written permission must be obtained from the author for any use beyond personal viewing; +- If you submit a Pull Request that modifies, supplements, or adds to the files in this directory or its subdirectories, unless otherwise agreed in writing, you agree that the copyright of your contribution is owned by hstyi and may be used and managed under the current license terms. All rights reserved. \ No newline at end of file diff --git a/plugins/vnc/build.gradle.kts b/plugins/vnc/build.gradle.kts new file mode 100644 index 0000000..5e4384a --- /dev/null +++ b/plugins/vnc/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + + +project.version = "0.0.1" + + +dependencies { + testImplementation(kotlin("test")) + testImplementation(project(":")) + implementation(files("${project.projectDir}/libs/trilead-ssh2-build217-jenkins-8.jar")) + compileOnly(project(":")) +} + + +apply(from = "$rootDir/plugins/common.gradle.kts") diff --git a/plugins/vnc/libs/trilead-ssh2-build217-jenkins-8.jar b/plugins/vnc/libs/trilead-ssh2-build217-jenkins-8.jar new file mode 100644 index 0000000000000000000000000000000000000000..29d2c2f1da25a220f3ab5173b27a3dba2a57871e GIT binary patch literal 262523 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ࣼ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

Hu*2Zvphe> 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 literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/java/com/glavsoft/core/SettingsChangedEvent.java b/plugins/vnc/src/main/java/com/glavsoft/core/SettingsChangedEvent.java new file mode 100644 index 0000000..0b7858c --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/core/SettingsChangedEvent.java @@ -0,0 +1,40 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.core; + +/** + * @author dime at tightvnc.com + */ +public class SettingsChangedEvent { + private final Object source; + + public SettingsChangedEvent(Object source) { + this.source = source; + } + + public Object getSource() { + return source; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/drawing/ColorDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/drawing/ColorDecoder.java new file mode 100644 index 0000000..f7059f2 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/drawing/ColorDecoder.java @@ -0,0 +1,144 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.drawing; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.transport.Transport; + +public class ColorDecoder { + protected byte redShift; + protected byte greenShift; + protected byte blueShift; + public short redMax; + public short greenMax; + public short blueMax; + public final int bytesPerPixel; + public final int bytesPerCPixel; + public final int bytesPerPixelTight; + private final byte[] buff; + + private int startShift; + private int startShiftCompact; + private int addShiftItem; + private final boolean isTightSpecific; + + public ColorDecoder(PixelFormat pf) { + redShift = pf.redShift; + greenShift = pf.greenShift; + blueShift = pf.blueShift; + redMax = pf.redMax; + greenMax = pf.greenMax; + blueMax = pf.blueMax; + bytesPerPixel = pf.bitsPerPixel / 8; + final long significant = redMax << redShift | greenMax << greenShift | blueMax << blueShift; + bytesPerCPixel = pf.depth <= 24 // as in RFB +// || 32 == pf.depth) // UltraVNC use this... :( + && 32 == pf.bitsPerPixel + && ((significant & 0x00ff000000L) == 0 || (significant & 0x000000ffL) == 0) + ? 3 + : bytesPerPixel; + bytesPerPixelTight = 24 == pf.depth && 32 == pf.bitsPerPixel ? 3 : bytesPerPixel; + buff = new byte[bytesPerPixel]; + if (0 == pf.bigEndianFlag) { + startShift = 0; + startShiftCompact = 0; + addShiftItem = 8; + } else { + startShift = pf.bitsPerPixel - 8; + startShiftCompact = Math.max(0, pf.depth - 8); + addShiftItem = -8; + } + isTightSpecific = 4==bytesPerPixel && 3==bytesPerPixelTight && + 255 == redMax && 255 == greenMax && 255 == blueMax; + } + + protected int readColor(Transport transport) throws TransportException { + return getColor(transport.readBytes(buff, 0, bytesPerPixel), 0); + } + + protected int readCompactColor(Transport transport) throws TransportException { + return getCompactColor(transport.readBytes(buff, 0, bytesPerCPixel), 0); + } + + protected int readTightColor(Transport transport) throws TransportException { + return getTightColor(transport.readBytes(buff, 0, bytesPerPixelTight), 0); + } + + /** + * Convert rfb encoded pixel color into 0x00rrggbb int value. + * @param rawColor - bytes are ordered in right sequence (little/big endian transformations already done + * @return 0x00rrggbb + */ + protected int convertColor(int rawColor) { + return 255 * (rawColor >> redShift & redMax) / redMax << 16 | + 255 * (rawColor >> greenShift & greenMax) / greenMax << 8 | + 255 * (rawColor >> blueShift & blueMax) / blueMax; + } + + public void fillRawComponents(byte[] comp, byte[] bytes, int offset) { + int rawColor = getRawTightColor(bytes, offset); + comp[0] = (byte) (rawColor >> redShift & redMax); + comp[1] = (byte) (rawColor >> greenShift & greenMax); + comp[2] = (byte) (rawColor >> blueShift & blueMax); + } + + public int getTightColor(byte[] bytes, int offset) { + return convertColor(getRawTightColor(bytes, offset)); + } + + private int getRawTightColor(byte[] bytes, int offset) { + if (isTightSpecific) + return (bytes[offset++] & 0xff)<<16 | + (bytes[offset++] & 0xff)<<8 | + bytes[offset] & 0xff; + else + return getRawColor(bytes, offset); + } + + protected int getColor(byte[] bytes, int offset) { + return convertColor(getRawColor(bytes, offset)); + } + + private int getRawColor(byte[] bytes, int offset) { + int shift = startShift; + int item = addShiftItem; + int rawColor = (bytes[offset++] & 0xff)<= 0; n--) { + pixels[i++] = palette[b >> n & 1]; + } + } + for (n = 7; n >= 8 - rect.width % 8; n--) { + pixels[i++] = palette[buffer[dy * rowBytes + dx] >> n & 1]; + } + i += this.width - rect.width; + } + } else { + // 3..255 colors (assuming bytesPixel == 4). + int i = 0; + for (int ly = rect.y; ly < rect.y + rect.height; ++ly) { + for (int lx = rect.x; lx < rect.x + rect.width; ++lx) { + int pixelsOffset = ly * this.width + lx; + pixels[pixelsOffset] = palette[buffer[i++] & 0xFF]; + } + } + } + lock.unlock(); + } + + /** + * Copy rectangle region from one position to another. Regions may be overlapped. + * + * @param srcX source rectangle x position + * @param srcY source rectangle y position + * @param dstRect destination rectangle + */ + public void copyRect(int srcX, int srcY, FramebufferUpdateRectangle dstRect) { + int startSrcY, endSrcY, dstY, deltaY; + if (srcY > dstRect.y) { + startSrcY = srcY; + endSrcY = srcY + dstRect.height; + dstY = dstRect.y; + deltaY = +1; + } else { + startSrcY = srcY + dstRect.height - 1; + endSrcY = srcY - 1; + dstY = dstRect.y + dstRect.height - 1; + deltaY = -1; + } + lock.lock(); + for (int y = startSrcY; y != endSrcY; y += deltaY) { + System.arraycopy(pixels, y * width + srcX, + pixels, dstY * width + dstRect.x, dstRect.width); + dstY += deltaY; + } + lock.unlock(); + } + + /** + * Fill rectangle region with specified colour + * + * @param color colour to fill with + * @param rect rectangle region positions and dimensions + */ + public void fillRect(int color, FramebufferUpdateRectangle rect) { + fillRect(color, rect.x, rect.y, rect.width, rect.height); + } + + /** + * Fill rectangle region with specified colour + * + * @param color colour to fill with + * @param x rectangle x position + * @param y rectangle y position + * @param width rectangle width + * @param height rectangle height + */ + public void fillRect(int color, int x, int y, int width, int height) { + lock.lock(); + int sy = y * this.width + x; + int ey = sy + height * this.width; + for (int i = sy; i < ey; i += this.width) { + Arrays.fill(pixels, i, i + width, color); + } + lock.unlock(); + } + + /** + * Reads color bytes (PIXEL) from transport, returns int combined RGB + * value consisting of the red component in bits 16-23, the green component + * in bits 8-15, and the blue component in bits 0-7. May be used directly for + * creation awt.Color object + */ + public int readPixelColor(Transport transport) throws TransportException { + return colorDecoder.readColor(transport); + } + + public int readTightPixelColor(Transport transport) throws TransportException { + return colorDecoder.readTightColor(transport); + } + + public ColorDecoder getColorDecoder() { + return colorDecoder; + } + + public int getCompactPixelColor(byte[] bytes, int offset) { + return colorDecoder.getCompactColor(bytes, offset); + } + + public int getPixelColor(byte[] bytes, int offset) { + return colorDecoder.getColor(bytes, offset); + } + + public int getBytesPerPixel() { + return colorDecoder.bytesPerPixel; + } + + public int getBytesPerCPixel() { + return colorDecoder.bytesPerCPixel; + } + + public int getBytesPerPixelTight() { + return colorDecoder.bytesPerPixelTight; + } + + public void fillColorBitmapWithColor(int[] bitmapData, int decodedOffset, int rlength, int color) { + while (rlength-- > 0) { + bitmapData[decodedOffset++] = color; + } + } + + /** + * Width of rendered image + * + * @return width + */ + public int getWidth() { + return width; + } + + /** + * Height of rendered image + * + * @return height + */ + public int getHeight() { + return height; + } + + /** + * Read and decode cursor image + * + * @param rect new cursor hot point position and cursor dimensions + * @throws TransportException + */ + public void createCursor(int[] cursorPixels, FramebufferUpdateRectangle rect) + throws TransportException { + synchronized (cursor.getLock()) { + cursor.createCursor(cursorPixels, rect.x, rect.y, rect.width, rect.height); + } + } + + /** + * Read and decode new cursor position + * + * @param rect cursor position + */ + public void decodeCursorPosition(FramebufferUpdateRectangle rect) { + synchronized (cursor.getLock()) { + cursor.updatePosition(rect.x, rect.y); + } + } + +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/drawing/SoftCursor.java b/plugins/vnc/src/main/java/com/glavsoft/drawing/SoftCursor.java new file mode 100644 index 0000000..7de9261 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/drawing/SoftCursor.java @@ -0,0 +1,91 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.drawing; + +/** + * Abstract class for operations with soft cursor positions, dimensions and + * hot point position. + */ +public abstract class SoftCursor { + + protected int hotX, hotY; + protected int x, y; + public int width, height; + public int rX, rY; + public int oldRX, oldRY; + public int oldWidth, oldHeight; + private final Object lock = new Object(); + + public SoftCursor(int hotX, int hotY, int width, int height) { + this.hotX = hotX; + this.hotY = hotY; + oldWidth = this.width = width; + oldHeight = this.height = height; + oldRX = rX = 0; + oldRY = rY = 0; + } + + /** + * Update cursor position + * + * @param newX + * @param newY + */ + public void updatePosition(int newX, int newY) { + oldRX = rX; oldRY = rY; + oldWidth = width; oldHeight = height; + x = newX; y = newY; + rX = x - hotX; rY = y - hotY; + } + + /** + * Set new cursor dimensions and hot point position + * + * @param hotX + * @param hotY + * @param width + * @param height + */ + public void setNewDimensions(int hotX, int hotY, int width, int height) { + this.hotX = hotX; + this.hotY = hotY; + oldWidth = this.width; + oldHeight = this.height; + oldRX = rX; oldRY = rY; + rX = x - hotX; rY = y - hotY; + this.width = width; + this.height = height; + } + + public void createCursor(int[] cursorPixels, int hotX, int hotY, int width, int height) { + createNewCursorImage(cursorPixels, hotX, hotY, width, height); + setNewDimensions(hotX, hotY, width, height); + } + + protected abstract void createNewCursorImage(int[] cursorPixels, int hotX, int hotY, int width, int height); + + public Object getLock() { + return lock; + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/AuthenticationFailedException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/AuthenticationFailedException.java new file mode 100644 index 0000000..834a705 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/AuthenticationFailedException.java @@ -0,0 +1,45 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +/** + * Throws when authentication was wrong + */ +@SuppressWarnings("serial") +public class AuthenticationFailedException extends ProtocolException { + + private String reason; + + public AuthenticationFailedException(String message) { + super(message); + } + public AuthenticationFailedException(String message, String reason) { + super(message); + this.reason = reason; + } + public String getReason() { + return reason; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/ClosedConnectionException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/ClosedConnectionException.java new file mode 100644 index 0000000..6885dc1 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/ClosedConnectionException.java @@ -0,0 +1,36 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +/** + * Throwed when connection closed (EOF) + */ +@SuppressWarnings("serial") +public class ClosedConnectionException extends TransportException { + + public ClosedConnectionException(Throwable exception) { + super(exception); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/CommonException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/CommonException.java new file mode 100644 index 0000000..08259de --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/CommonException.java @@ -0,0 +1,39 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +@SuppressWarnings("serial") +public class CommonException extends Exception { + + public CommonException(Throwable exception) { + super(exception); + } + public CommonException(String message, Throwable exception) { + super(message, exception); + } + public CommonException(String message) { + super(message); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/CouldNotConnectException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/CouldNotConnectException.java new file mode 100644 index 0000000..d9ee125 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/CouldNotConnectException.java @@ -0,0 +1,33 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +@SuppressWarnings("serial") +public class CouldNotConnectException extends TransportException { + + public CouldNotConnectException(Throwable exception) { + super(exception); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/CryptoException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/CryptoException.java new file mode 100644 index 0000000..ce7e953 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/CryptoException.java @@ -0,0 +1,34 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +/** + * Throws when problem with DES (or smth else) cryptosystem occured + */ +@SuppressWarnings("serial") +public class CryptoException extends FatalException { + public CryptoException(String message, Throwable exception) { + super(message, exception); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/FatalException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/FatalException.java new file mode 100644 index 0000000..4d7ad77 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/FatalException.java @@ -0,0 +1,34 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +/** + * Trhows when further normal program execution unavailable + */ +@SuppressWarnings("serial") +public class FatalException extends CommonException { + public FatalException(String message, Throwable e) { + super(message, e); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/ProtocolException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/ProtocolException.java new file mode 100644 index 0000000..7c06aee --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/ProtocolException.java @@ -0,0 +1,31 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +@SuppressWarnings("serial") +public class ProtocolException extends CommonException { + public ProtocolException(String message) { + super(message); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/TransportException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/TransportException.java new file mode 100644 index 0000000..25f4659 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/TransportException.java @@ -0,0 +1,39 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + + +@SuppressWarnings("serial") +public class TransportException extends CommonException { + public TransportException(String message, Throwable exception) { + super(message, exception); + } + public TransportException(Throwable exception) { + super(exception); + } + public TransportException(String message) { + super(message); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedProtocolVersionException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedProtocolVersionException.java new file mode 100644 index 0000000..b335b30 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedProtocolVersionException.java @@ -0,0 +1,32 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +@SuppressWarnings("serial") +public class UnsupportedProtocolVersionException extends ProtocolException { + public UnsupportedProtocolVersionException(String message) { + super(message); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedSecurityTypeException.java b/plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedSecurityTypeException.java new file mode 100644 index 0000000..39233ff --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/exceptions/UnsupportedSecurityTypeException.java @@ -0,0 +1,32 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.exceptions; + +@SuppressWarnings("serial") +public class UnsupportedSecurityTypeException extends ProtocolException { + public UnsupportedSecurityTypeException(String message) { + super(message); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/ClipboardController.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/ClipboardController.java new file mode 100644 index 0000000..c0b8642 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/ClipboardController.java @@ -0,0 +1,58 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb; + +/** + * Interface for handling clipboard texts + */ +public interface ClipboardController extends IChangeSettingsListener { + void updateSystemClipboard(byte[] bytes); + + /** + * Get text clipboard contens when needed send to remote, or null vise versa + * Implement this method such a way in swing context, because swing's clipboard + * update listener invoked only on DataFlavor changes not content changes. + * Implement as returned null on systems where clipboard listeners work correctly. + * + * @return clipboad string contents if it is changed from last method call + * or null when clipboard contains non text object or clipboard contents didn't changed + */ + String getRenewedClipboardText(); + + /** + * Returns clipboard text content previously retrieved frim system clipboard by + * updateSavedClippoardContent() + * + * @return clipboard text content + */ + String getClipboardText(); + + /** + * Enable/disable clipboard transfer + * + * @param enable + */ + void setEnabled(boolean enable); + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/IChangeSettingsListener.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/IChangeSettingsListener.java new file mode 100644 index 0000000..f4d31b6 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/IChangeSettingsListener.java @@ -0,0 +1,33 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb; + +import com.glavsoft.core.SettingsChangedEvent; + +/** + * @author dime at tightvnc.com + */ +public interface IChangeSettingsListener { + void settingsChanged(SettingsChangedEvent event); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/IRepaintController.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/IRepaintController.java new file mode 100644 index 0000000..2cd3fe1 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/IRepaintController.java @@ -0,0 +1,41 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.rfb.encoding.decoder.FramebufferUpdateRectangle; +import com.glavsoft.transport.Transport; + +/** + * Interface for sending repaint event from worker thread to GUI thread + */ +public interface IRepaintController extends IChangeSettingsListener { + void repaintBitmap(FramebufferUpdateRectangle rect); + void repaintBitmap(int x, int y, int width, int height); + void repaintCursor(); + void updateCursorPosition(short x, short y); + Renderer createRenderer(Transport transport, int width, int height, PixelFormat pixelFormat); + void setPixelFormat(PixelFormat pixelFormat); +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/IRequestString.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/IRequestString.java new file mode 100644 index 0000000..29da71d --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/IRequestString.java @@ -0,0 +1,28 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb; + +public interface IRequestString { + String getResult(); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/IRfbSessionListener.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/IRfbSessionListener.java new file mode 100644 index 0000000..80cfa12 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/IRfbSessionListener.java @@ -0,0 +1,38 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb; + +/** + * Fires Rfb session [start]-stop events + */ +public interface IRfbSessionListener { + + /** + * Fired after rfb session closed (threads stopped, other resources fried). + * Note: this event may be fired from unpredictable thread. + * @param reason reason to show why session closed + */ + void rfbSessionStopped(String reason); + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/RfbCapabilityInfo.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/RfbCapabilityInfo.java new file mode 100644 index 0000000..c26052d --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/RfbCapabilityInfo.java @@ -0,0 +1,135 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * Structure used to describe protocol options such as tunneling methods, + * authentication schemes and message types (protocol versions 3.7t, 3.8t). + * typedef struct _rfbCapabilityInfo { + * CARD32 code; // numeric identifier + * CARD8 vendorSignature[4]; // vendor identification + * CARD8 nameSignature[8]; // abbreviated option name + * } rfbCapabilityInfo; + */ +public class RfbCapabilityInfo { + /* + * Vendors known by TightVNC: standard VNC/RealVNC, TridiaVNC, and TightVNC. + * #define rfbStandardVendor "STDV" + * #define rfbTridiaVncVendor "TRDV" + * #define rfbTightVncVendor "TGHT" + */ + public static final String VENDOR_STANDARD = "STDV"; + public static final String VENDOR_TRIADA = "TRDV"; + public static final String VENDOR_TIGHT = "TGHT"; + + public static final String TUNNELING_NO_TUNNEL = "NOTUNNEL"; + + public static final String AUTHENTICATION_NO_AUTH = "NOAUTH__"; + public static final String AUTHENTICATION_VNC_AUTH ="VNCAUTH_"; + + public static final String ENCODING_COPYRECT = "COPYRECT"; + public static final String ENCODING_HEXTILE = "HEXTILE_"; + public static final String ENCODING_ZLIB = "ZLIB____"; + public static final String ENCODING_ZRLE = "ZRLE____"; + public static final String ENCODING_RRE = "RRE_____"; + public static final String ENCODING_TIGHT = "TIGHT___"; + // "Pseudo" encoding types + public static final String ENCODING_RICH_CURSOR = "RCHCURSR"; + public static final String ENCODING_CURSOR_POS = "POINTPOS"; + public static final String ENCODING_DESKTOP_SIZE = "NEWFBSIZ"; + + private int code; + private String vendorSignature; + private String nameSignature; + private boolean enable; + + public RfbCapabilityInfo(int code, String vendorSignature, String nameSignature) { + this.code = code; + this.vendorSignature = vendorSignature; + this.nameSignature = nameSignature; + enable = true; + } + + public RfbCapabilityInfo() { + this(0, "", ""); + } + + public RfbCapabilityInfo readFrom(Transport transport) throws TransportException { + code = transport.readInt32(); + vendorSignature = transport.readString(4); + nameSignature = transport.readString(8); + enable = true; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RfbCapabilityInfo that = (RfbCapabilityInfo) o; + return code == that.code && + nameSignature.equals(that.nameSignature) && + vendorSignature.equals(that.vendorSignature); + } + + @Override + public int hashCode() { + int result = code; + result = 31 * result + vendorSignature.hashCode(); + result = 31 * result + nameSignature.hashCode(); + return result; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public int getCode() { + return code; + } + + public String getVendorSignature() { + return vendorSignature; + } + + public String getNameSignature() { + return nameSignature; + } + + public boolean isEnabled() { + return enable; + } + + @Override + public String toString() { + return "RfbCapabilityInfo{" + + "code=" + code + + ", vendorSignature='" + vendorSignature + '\'' + + ", nameSignature='" + nameSignature + '\'' + + '}'; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientCutTextMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientCutTextMessage.java new file mode 100644 index 0000000..c4e8e41 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientCutTextMessage.java @@ -0,0 +1,66 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +import java.nio.charset.Charset; +import java.util.Arrays; + +import static com.glavsoft.utils.Strings.getBytesWithCharset; + +/** + * ClientCutText + * The client has new ISO 8859-1 (Latin-1) text in its cut buffer. Ends of lines are repre- + * sented by the linefeed / newline character (value 10) alone. No carriage-return (value + * 13) is needed. There is currently no way to transfer text outside the Latin-1 character + * set. + * 1 - U8 - 6 + * 3 - - padding + * 4 - U32 - length + * length - U8 array - text + */ +public class ClientCutTextMessage implements ClientToServerMessage { + private final byte [] bytes; + + public ClientCutTextMessage(String str, Charset charset) { + final byte[] b = charset != null? getBytesWithCharset(str, charset): str.getBytes(); + this.bytes = Arrays.copyOf(b, b.length); + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.CLIENT_CUT_TEXT.id) + .zero(3) // padding + .writeInt32(bytes.length) + .write(bytes) + .flush(); + } + + @Override + public String toString() { + return "ClientCutTextMessage: [length: " + bytes.length +", text: ...]"; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientMessageType.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientMessageType.java new file mode 100644 index 0000000..e4ba76e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientMessageType.java @@ -0,0 +1,66 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +/** + * @author dime at glavsoft.com + */ +public enum ClientMessageType { + SET_PIXEL_FORMAT(0), + SET_ENCODINGS(2), + FRAMEBUFFER_UPDATE_REQUEST(3), + KEY_EVENT(4), + POINTER_EVENT(5), + CLIENT_CUT_TEXT(6), + + VIDEO_RECTANGLE_SELECTION(151), + VIDEO_FREEZE(152); + + final public int id; + + ClientMessageType(int id) { + this.id = id; + } + + private static final ClientMessageType [] standardTypes = + {SET_PIXEL_FORMAT, SET_ENCODINGS, FRAMEBUFFER_UPDATE_REQUEST, KEY_EVENT, POINTER_EVENT, CLIENT_CUT_TEXT}; + + public static boolean isStandardType(ClientMessageType type) { + for (final ClientMessageType it : standardTypes) { + if (it == type) { + return true; + } + } + return false; + } + + public static ClientMessageType byId(int id) { + for (ClientMessageType type : values()) { + if (type.id == id) + return type; + } + throw new IllegalArgumentException("Unsupported client message type: " + id); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientToServerMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientToServerMessage.java new file mode 100644 index 0000000..78d82a2 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/ClientToServerMessage.java @@ -0,0 +1,31 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +public interface ClientToServerMessage { + void send(Transport transport) throws TransportException; +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/FramebufferUpdateRequestMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/FramebufferUpdateRequestMessage.java new file mode 100644 index 0000000..15585e7 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/FramebufferUpdateRequestMessage.java @@ -0,0 +1,64 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + + +public class FramebufferUpdateRequestMessage implements ClientToServerMessage { + private final boolean incremental; + private final int height; + private final int width; + private final int y; + private final int x; + + public FramebufferUpdateRequestMessage(int x, int y, int width, + int height, boolean incremental) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.incremental = incremental; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.FRAMEBUFFER_UPDATE_REQUEST.id) + .writeByte(incremental ? 1 : 0) + .writeInt16(x) + .writeInt16(y) + .writeInt16(width) + .writeInt16(height) + .flush(); + } + + @Override + public String toString() { + return "FramebufferUpdateRequestMessage: [x: " + x + " y: " + y + + " width: " + width + " height: " + height + + " incremental: " + incremental + "]"; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/KeyEventMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/KeyEventMessage.java new file mode 100644 index 0000000..7cca4c2 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/KeyEventMessage.java @@ -0,0 +1,65 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * A key press or release. Down-flag is non-zero (true) if the key is now pressed, zero + * (false) if it is now released. The key itself is specified using the "keysym" values + * defined by the X Window System. + * 1 - U8 - message-type + * 1 - U8 - down-flag + * 2 - - - padding + * 4 - U32 - key + * For most ordinary keys, the "keysym" is the same as the corresponding ASCII value. + * For full details, see The Xlib Reference Manual, published by O'Reilly & Associates, + * or see the header file <X11/keysymdef.h> from any X Window System installation. + */ +public class KeyEventMessage implements ClientToServerMessage { + + private final int key; + private final boolean downFlag; + + public KeyEventMessage(int key, boolean downFlag) { + this.downFlag = downFlag; + this.key = key; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.KEY_EVENT.id) + .writeByte(downFlag ? 1 : 0) + .zero(2) // padding + .write(key) + .flush(); + } + + @Override + public String toString() { + return "[KeyEventMessage: [down-flag: "+downFlag + ", key: " + key +"("+Integer.toHexString(key)+")]"; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/PointerEventMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/PointerEventMessage.java new file mode 100644 index 0000000..8ef3a4b --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/PointerEventMessage.java @@ -0,0 +1,55 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +public class PointerEventMessage implements ClientToServerMessage { + private final byte buttonMask; + private final short x; + private final short y; + + public PointerEventMessage(byte buttonMask, short x, short y) { + this.buttonMask = buttonMask; + this.x = x; + this.y = y; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.POINTER_EVENT.id) + .writeByte(buttonMask) + .writeInt16(x) + .writeInt16(y) + .flush(); + } + + @Override + public String toString() { + return "PointerEventMessage: [x: "+ x +", y: "+ y + ", button-mask: " + + buttonMask +"]"; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetEncodingsMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetEncodingsMessage.java new file mode 100644 index 0000000..db0eef6 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetEncodingsMessage.java @@ -0,0 +1,60 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.encoding.EncodingType; +import com.glavsoft.transport.Transport; + +import java.util.Set; + +public class SetEncodingsMessage implements ClientToServerMessage { + private final Set encodings; + + public SetEncodingsMessage(Set set) { + this.encodings = set; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.SET_ENCODINGS.id) + .zero(1) // padding byte + .writeInt16(encodings.size()); + for (EncodingType enc : encodings) { + transport.writeInt32(enc.getId()); + } + transport.flush(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SetEncodingsMessage: [encodings: "); + for (EncodingType enc : encodings) { + sb.append(enc.name()).append(','); + } + sb.setLength(sb.length()-1); + return sb.append(']').toString(); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetPixelFormatMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetPixelFormatMessage.java new file mode 100644 index 0000000..b784874 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/SetPixelFormatMessage.java @@ -0,0 +1,45 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.transport.Transport; + +public class SetPixelFormatMessage implements ClientToServerMessage { + private final PixelFormat pixelFormat; + + public SetPixelFormatMessage(PixelFormat pixelFormat) { + this.pixelFormat = pixelFormat; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.SET_PIXEL_FORMAT.id) + .zero(3); + pixelFormat.send(transport); + transport.flush(); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoFreezeMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoFreezeMessage.java new file mode 100644 index 0000000..97a3332 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoFreezeMessage.java @@ -0,0 +1,46 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * @author dime at glavsoft.com + */ +public class VideoFreezeMessage implements ClientToServerMessage { + + private boolean freeze; + + public VideoFreezeMessage(boolean freeze) { + this.freeze = freeze; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.VIDEO_FREEZE.id) + .writeByte(freeze ? 1 : 0) + .flush(); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoRectangleSelectionMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoRectangleSelectionMessage.java new file mode 100644 index 0000000..7fcd6c5 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/client/VideoRectangleSelectionMessage.java @@ -0,0 +1,56 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.client; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * @author dime at glavsoft.com + */ +public class VideoRectangleSelectionMessage implements ClientToServerMessage { + + private final int x; + private final int y; + private final int width; + private final int height; + + public VideoRectangleSelectionMessage(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void send(Transport transport) throws TransportException { + transport.writeByte(ClientMessageType.VIDEO_RECTANGLE_SELECTION.id) + .zero(1) // padding + .writeInt16(x) + .writeInt16(y) + .writeInt16(width) + .writeInt16(height) + .flush(); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/EncodingType.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/EncodingType.java new file mode 100644 index 0000000..c0c90b5 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/EncodingType.java @@ -0,0 +1,156 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding; + +import com.glavsoft.rfb.encoding.decoder.CopyRectDecoder; +import com.glavsoft.rfb.encoding.decoder.CursorPosDecoder; +import com.glavsoft.rfb.encoding.decoder.Decoder; +import com.glavsoft.rfb.encoding.decoder.DesctopSizeDecoder; +import com.glavsoft.rfb.encoding.decoder.FakeDecoder; +import com.glavsoft.rfb.encoding.decoder.HextileDecoder; +import com.glavsoft.rfb.encoding.decoder.RREDecoder; +import com.glavsoft.rfb.encoding.decoder.RawDecoder; +import com.glavsoft.rfb.encoding.decoder.RichCursorDecoder; +import com.glavsoft.rfb.encoding.decoder.TightDecoder; +import com.glavsoft.rfb.encoding.decoder.ZRLEDecoder; +import com.glavsoft.rfb.encoding.decoder.ZlibDecoder; + +import java.util.LinkedHashSet; + +/** + * Encoding types + */ +public enum EncodingType { + /** + * Desktop data representes as raw bytes stream + */ + RAW_ENCODING(0, "Raw", RawDecoder.class), + /** + * Specfies encodings which allow to copy part of image in client's + * framebuffer from one place to another. + */ + COPY_RECT(1, "CopyRect", CopyRectDecoder.class), + RRE(2, "RRE", RREDecoder.class), + /** + * Hextile encoding, uses palettes, filling and raw subencoding + */ + HEXTILE(5, "Hextile", HextileDecoder.class), + /** + * This encoding is like raw but previously all data compressed with zlib. + */ + ZLIB(6, "ZLib", ZlibDecoder.class), + /** + * Tight Encoding for slow connection. It is uses raw data, palettes, filling + * and jpeg subencodings + */ + TIGHT(7, "Tight", TightDecoder.class), + //ZlibHex(8), + /** + * ZRLE Encoding is like Hextile but previously all data compressed with zlib. + */ + ZRLE(16, "ZRLE", ZRLEDecoder.class), + + /** + * Rich Cursor pseudo encoding which allows to transfer cursor shape + * with transparency + */ + RICH_CURSOR(0xFFFFFF11, "RichCursor", RichCursorDecoder.class), + /** + * Desktop Size Pseudo encoding allows to notificate client about + * remote screen resolution changed. + */ + DESKTOP_SIZE(0xFFFFFF21, "DesctopSize", DesctopSizeDecoder.class), + /** + * Cusros position encoding allows to transfer remote cursor position to + * client side. + */ + CURSOR_POS(0xFFFFFF18, "CursorPos", CursorPosDecoder.class), + + COMPRESS_LEVEL_0(0xFFFFFF00 + 0, "CompressionLevel0", FakeDecoder.class), + COMPRESS_LEVEL_1(0xFFFFFF00 + 1, "CompressionLevel1", null), + COMPRESS_LEVEL_2(0xFFFFFF00 + 2, "CompressionLevel2", null), + COMPRESS_LEVEL_3(0xFFFFFF00 + 3, "CompressionLevel3", null), + COMPRESS_LEVEL_4(0xFFFFFF00 + 4, "CompressionLevel4", null), + COMPRESS_LEVEL_5(0xFFFFFF00 + 5, "CompressionLevel5", null), + COMPRESS_LEVEL_6(0xFFFFFF00 + 6, "CompressionLevel6", null), + COMPRESS_LEVEL_7(0xFFFFFF00 + 7, "CompressionLevel7", null), + COMPRESS_LEVEL_8(0xFFFFFF00 + 8, "CompressionLevel8", null), + COMPRESS_LEVEL_9(0xFFFFFF00 + 9, "CompressionLevel9", null), + + JPEG_QUALITY_LEVEL_0(0xFFFFFFE0 + 0, "JpegQualityLevel0", FakeDecoder.class), + JPEG_QUALITY_LEVEL_1(0xFFFFFFE0 + 1, "JpegQualityLevel1", null), + JPEG_QUALITY_LEVEL_2(0xFFFFFFE0 + 2, "JpegQualityLevel2", null), + JPEG_QUALITY_LEVEL_3(0xFFFFFFE0 + 3, "JpegQualityLevel3", null), + JPEG_QUALITY_LEVEL_4(0xFFFFFFE0 + 4, "JpegQualityLevel4", null), + JPEG_QUALITY_LEVEL_5(0xFFFFFFE0 + 5, "JpegQualityLevel5", null), + JPEG_QUALITY_LEVEL_6(0xFFFFFFE0 + 6, "JpegQualityLevel6", null), + JPEG_QUALITY_LEVEL_7(0xFFFFFFE0 + 7, "JpegQualityLevel7", null), + JPEG_QUALITY_LEVEL_8(0xFFFFFFE0 + 8, "JpegQualityLevel8", null), + JPEG_QUALITY_LEVEL_9(0xFFFFFFE0 + 9, "JpegQualityLevel9", null); + + private final int id; + private final String name; + public final Class klass; + + private EncodingType(int id, String name, Class klass) { + this.id = id; + this.name = name; + this.klass = klass; + } + + public int getId() { + return id; + } + public String getName() { + return name; + } + + public static final LinkedHashSet ordinaryEncodings = new LinkedHashSet(); + static { + ordinaryEncodings.add(ZRLE); + ordinaryEncodings.add(TIGHT); + ordinaryEncodings.add(ZLIB); + ordinaryEncodings.add(HEXTILE); + ordinaryEncodings.add(RRE); + ordinaryEncodings.add(COPY_RECT); + ordinaryEncodings.add(RAW_ENCODING); + } + + public static final LinkedHashSet pseudoEncodings = new LinkedHashSet(); + static { + pseudoEncodings.add(RICH_CURSOR); + pseudoEncodings.add(CURSOR_POS); + pseudoEncodings.add(DESKTOP_SIZE); + } + + public static EncodingType byId(int id) { + // TODO needs to speedup with hash usage? + for (EncodingType type : values()) { + if (type.getId() == id) + return type; + } + throw new IllegalArgumentException("Unsupported encoding code: " + id); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/PixelFormat.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/PixelFormat.java new file mode 100644 index 0000000..4857b45 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/PixelFormat.java @@ -0,0 +1,185 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * Pixel Format: + * 1 - U8 - bits-per-pixel + * 1 - U8 - depth + * 1 - U8 - big-endian-flag + * 1 - U8 - true-color-flag + * 2 - U16 - red-max + * 2 - U16 - green-max + * 2 - U16 - blue-max + * 1 - U8 - red-shift + * 1 - U8 - green-shift + * 1 - U8 - blue-shift + * 3 - - padding + */ +public class PixelFormat { + public byte bitsPerPixel; + public byte depth; + public byte bigEndianFlag; + public byte trueColourFlag; + public short redMax; + public short greenMax; + public short blueMax; + public byte redShift; + public byte greenShift; + public byte blueShift; + + public void fill(Transport transport) throws TransportException { + bitsPerPixel = transport.readByte(); + depth = transport.readByte(); + bigEndianFlag = transport.readByte(); + trueColourFlag = transport.readByte(); + redMax = transport.readInt16(); + greenMax = transport.readInt16(); + blueMax = transport.readInt16(); + redShift = transport.readByte(); + greenShift = transport.readByte(); + blueShift = transport.readByte(); + transport.readBytes(3); // skip padding bytes + } + + public void send(Transport transport) throws TransportException { + transport.write(bitsPerPixel) + .write(depth) + .write(bigEndianFlag) + .write(trueColourFlag) + .write(redMax) + .write(greenMax) + .write(blueMax) + .write(redShift) + .write(greenShift) + .write(blueShift) + .writeInt16(0) // padding bytes + .writeByte(0); // padding bytes + } + + public static PixelFormat create24bitColorDepthPixelFormat(int bigEndianFlag) { + final PixelFormat pixelFormat = new PixelFormat(); + pixelFormat.bigEndianFlag = (byte) bigEndianFlag; + pixelFormat.bitsPerPixel = 32; + pixelFormat.blueMax = 255; + pixelFormat.blueShift = 0; + pixelFormat.greenMax = 255; + pixelFormat.greenShift = 8; + pixelFormat.redMax = 255; + pixelFormat.redShift = 16; + pixelFormat.depth = 24; + pixelFormat.trueColourFlag = 1; + return pixelFormat; + } + + /** + * specifies 65536 colors, 5bit per Red, 6bit per Green, 5bit per Blue + */ + public static PixelFormat create16bitColorDepthPixelFormat(int bigEndianFlag) { + final PixelFormat pixelFormat = new PixelFormat(); + pixelFormat.bigEndianFlag = (byte) bigEndianFlag; + pixelFormat.bitsPerPixel = 16; + pixelFormat.blueMax = 31; + pixelFormat.blueShift = 0; + pixelFormat.greenMax = 63; + pixelFormat.greenShift = 5; + pixelFormat.redMax = 31; + pixelFormat.redShift = 11; + pixelFormat.depth = 16; + pixelFormat.trueColourFlag = 1; + return pixelFormat; + } + + /** + * specifies 256 colors, 2bit per Blue, 3bit per Green & Red + */ + public static PixelFormat create8bitColorDepthBGRPixelFormat(int bigEndianFlag) { + final PixelFormat pixelFormat = new PixelFormat(); + pixelFormat.bigEndianFlag = (byte) bigEndianFlag; + pixelFormat.bitsPerPixel = 8; + pixelFormat.redMax = 7; + pixelFormat.redShift = 0; + pixelFormat.greenMax = 7; + pixelFormat.greenShift = 3; + pixelFormat.blueMax = 3; + pixelFormat.blueShift = 6; + pixelFormat.depth = 8; + pixelFormat.trueColourFlag = 1; + return pixelFormat; + } + + /** + * specifies 64 colors, 2bit per Red, Green & Blue + */ + public static PixelFormat create6bitColorDepthPixelFormat(int bigEndianFlag) { + final PixelFormat pixelFormat = new PixelFormat(); + pixelFormat.bigEndianFlag = (byte) bigEndianFlag; + pixelFormat.bitsPerPixel = 8; + pixelFormat.blueMax = 3; + pixelFormat.blueShift = 0; + pixelFormat.greenMax = 3; + pixelFormat.greenShift = 2; + pixelFormat.redMax = 3; + pixelFormat.redShift = 4; + pixelFormat.depth = 6; + pixelFormat.trueColourFlag = 1; + return pixelFormat; + } + + /** + * specifies 8 colors, 1bit per Red, Green & Blue + */ + public static PixelFormat create3bitColorDepthPixelFormat(int bigEndianFlag) { + final PixelFormat pixelFormat = new PixelFormat(); + pixelFormat.bigEndianFlag = (byte) bigEndianFlag; + pixelFormat.bitsPerPixel = 8; + pixelFormat.blueMax = 1; + pixelFormat.blueShift = 0; + pixelFormat.greenMax = 1; + pixelFormat.greenShift = 1; + pixelFormat.redMax = 1; + pixelFormat.redShift = 2; + pixelFormat.depth = 3; + pixelFormat.trueColourFlag = 1; + return pixelFormat; + } + + @Override + public String toString() { + return "PixelFormat: [bits-per-pixel: " + String.valueOf(0xff & bitsPerPixel) + + ", depth: " + String.valueOf(0xff & depth) + + ", big-endian-flag: " + String.valueOf(0xff & bigEndianFlag) + + ", true-color-flag: " + String.valueOf(0xff & trueColourFlag) + + ", red-max: " + String.valueOf(0xffff & redMax) + + ", green-max: " + String.valueOf(0xffff & greenMax) + + ", blue-max: " + String.valueOf(0xffff & blueMax) + + ", red-shift: " + String.valueOf(0xff & redShift) + + ", green-shift: " + String.valueOf(0xff & greenShift) + + ", blue-shift: " + String.valueOf(0xff & blueShift) + + "]"; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/ServerInitMessage.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/ServerInitMessage.java new file mode 100644 index 0000000..4879b5a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/ServerInitMessage.java @@ -0,0 +1,77 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * Struct filled from the ServerInit message + * 2 - U16 - framebuffer-width + * 2 - U16 - framebuffer-height + * 16 - PixelFormat - server-pixel-format + * 4 - U32 - name-length + * name-length - U8 array - name-string + */ +public class ServerInitMessage { + protected String name; + protected int framebufferWidth; + protected int framebufferHeight; + protected PixelFormat pixelFormat; + + public ServerInitMessage readFrom(Transport transport) throws TransportException { + framebufferWidth = transport.readUInt16(); + framebufferHeight = transport.readUInt16(); + pixelFormat = new PixelFormat(); + pixelFormat.fill(transport); + name = transport.readString(); + return this; + } + + public int getFramebufferWidth() { + return framebufferWidth; + } + + public int getFramebufferHeight() { + return framebufferHeight; + } + + public PixelFormat getPixelFormat() { + return pixelFormat; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "ServerInitMessage{" + + "name='" + name + '\'' + + ", framebufferWidth=" + framebufferWidth + + ", framebufferHeight=" + framebufferHeight + + ", pixelFormat=" + pixelFormat + + '}'; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ByteBuffer.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ByteBuffer.java new file mode 100644 index 0000000..60eb9c2 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ByteBuffer.java @@ -0,0 +1,65 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +/** + * Resizeable to needed length byte buffer + * Singleton for share among decoders. + */ +public class ByteBuffer { + private static ThreadLocal threadLocal = new ThreadLocal() { + @Override + protected ByteBuffer initialValue() { + return new ByteBuffer(); + } + }; + private byte [] buffer = new byte[0]; + + private ByteBuffer() { /*empty*/ } + + public static ByteBuffer getInstance() { + return threadLocal.get(); + } + + public static void removeInstance() { + threadLocal.remove(); + } + + /** + * Checks for buffer capacity is enougth ( < length) and enlarge it if not + */ + public void correctBufferCapacity(int length) { + // procondition: buffer != null + assert (buffer != null); + if (buffer.length < length) { + buffer = new byte[length]; + } + } + + public byte[] getBuffer(int length) { + correctBufferCapacity(length); + return buffer; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CopyRectDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CopyRectDecoder.java new file mode 100644 index 0000000..11bff37 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CopyRectDecoder.java @@ -0,0 +1,41 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +public class CopyRectDecoder extends Decoder { + + @Override + public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + int srcX = transport.readUInt16(); + int srcY = transport.readUInt16(); + if (rect.width == 0 || rect.height == 0) return; + renderer.copyRect(srcX, srcY, rect); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CursorPosDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CursorPosDecoder.java new file mode 100644 index 0000000..0132cf6 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/CursorPosDecoder.java @@ -0,0 +1,39 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * @author dime at glavsoft.com + */ +public class CursorPosDecoder extends Decoder { + + @Override + public void decode(Transport transport, Renderer renderer, FramebufferUpdateRectangle rect) throws TransportException { + renderer.decodeCursorPosition(rect); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/Decoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/Decoder.java new file mode 100644 index 0000000..2273389 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/Decoder.java @@ -0,0 +1,43 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + + +public abstract class Decoder { + /** + * Decode rectangle data. + */ + abstract public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException; + + /** + * Reset decoder when needed. Ex. reset ZLib stream inflaters for Z* and Tight decoders. + */ + public void reset() { /*empty*/ } + +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/DesctopSizeDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/DesctopSizeDecoder.java new file mode 100644 index 0000000..f44077b --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/DesctopSizeDecoder.java @@ -0,0 +1,39 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * @author dime at glavsoft.com + */ +public class DesctopSizeDecoder extends Decoder { + + @Override + public void decode(Transport transport, Renderer renderer, FramebufferUpdateRectangle rect) throws TransportException { + // do nothing, all real job will be done in ReceiverTask.framebufferUpdateMessage() + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FakeDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FakeDecoder.java new file mode 100644 index 0000000..04d8e9d --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FakeDecoder.java @@ -0,0 +1,39 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * @author dime at glavsoft.com + */ +public class FakeDecoder extends Decoder { + + @Override + public void decode(Transport transport, Renderer renderer, FramebufferUpdateRectangle rect) throws TransportException { + throw new IllegalStateException("Improper use of fake decoder"); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FramebufferUpdateRectangle.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FramebufferUpdateRectangle.java new file mode 100644 index 0000000..f8220e7 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/FramebufferUpdateRectangle.java @@ -0,0 +1,76 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.encoding.EncodingType; +import com.glavsoft.transport.Transport; + +/** + * Header for framebuffer-update-rectangle header server message + * 2 - U16 - x-position + * 2 - U16 - y-position + * 2 - U16 - width + * 2 - U16 - height + * 4 - S32 - encoding-type + * and then follows the pixel data in the specified encoding + */ +public class FramebufferUpdateRectangle { + public int x; + public int y; + public int width; + public int height; + private EncodingType encodingType; + + public FramebufferUpdateRectangle() { + // nop + } + + public FramebufferUpdateRectangle(int x, int y, int w, int h) { + this.x = x; this.y = y; + width = w; height = h; + } + + public void fill(Transport transport) throws TransportException { + x = transport.readUInt16(); + y = transport.readUInt16(); + width = transport.readUInt16(); + height = transport.readUInt16(); + int encoding = transport.readInt32(); + encodingType = EncodingType.byId(encoding); + } + + public EncodingType getEncodingType() { + return encodingType; + } + + @Override + public String toString() { + return "FramebufferUpdateRect: [x: " + x + ", y: " + y + + ", width: " + width + ", height: " + height + + ", encodingType: " + encodingType + + "]"; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/HextileDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/HextileDecoder.java new file mode 100644 index 0000000..29f5b13 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/HextileDecoder.java @@ -0,0 +1,103 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + + +public class HextileDecoder extends Decoder { + private static final int DEFAULT_TILE_SIZE = 16; + private static final int RAW_MASK = 1; + private static final int BACKGROUND_SPECIFIED_MASK = 2; + private static final int FOREGROUND_SPECIFIED_MASK = 4; + private static final int ANY_SUBRECTS_MASK = 8; + private static final int SUBRECTS_COLOURED_MASK = 16; + private static final int FG_COLOR_INDEX = 0; + private static final int BG_COLOR_INDEX = 1; + + @Override + public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + if (rect.width == 0 || rect.height == 0) return; + int[] colors = new int[] {-1, -1}; + int maxX = rect.x + rect.width; + int maxY = rect.y + rect.height; + for (int tileY = rect.y; tileY < maxY; + tileY += DEFAULT_TILE_SIZE) { + int tileHeight = Math.min(maxY - tileY, DEFAULT_TILE_SIZE); + for (int tileX = rect.x; tileX < maxX; + tileX += DEFAULT_TILE_SIZE) { + int tileWidth = Math.min(maxX - tileX, DEFAULT_TILE_SIZE); + decodeTile(transport, renderer, colors, tileX, + tileY, tileWidth, tileHeight); + } + } + + } + + private void decodeTile(Transport transport, + Renderer renderer, int[] colors, + int tileX, int tileY, int tileWidth, int tileHeight) + throws TransportException { + int subencoding = transport.readUInt8(); + if ((subencoding & RAW_MASK) != 0) { + RawDecoder.getInstance().decode(transport, renderer, + tileX, tileY, tileWidth, tileHeight); + return; + } + + if ((subencoding & BACKGROUND_SPECIFIED_MASK) != 0) { + colors[BG_COLOR_INDEX] = renderer.readPixelColor(transport); + } + renderer.fillRect(colors[BG_COLOR_INDEX], + tileX, tileY, tileWidth, tileHeight); + + if ((subencoding & FOREGROUND_SPECIFIED_MASK) != 0) { + colors[FG_COLOR_INDEX] = renderer.readPixelColor(transport); + } + + if ((subencoding & ANY_SUBRECTS_MASK) == 0) + return; + + int numberOfSubrectangles = transport.readUInt8(); + boolean colorSpecified = (subencoding & SUBRECTS_COLOURED_MASK) != 0; + for (int i = 0; i < numberOfSubrectangles; ++i) { + int color = colorSpecified ? renderer.readPixelColor(transport) : colors[FG_COLOR_INDEX]; + colors[FG_COLOR_INDEX] = color; + byte dimensions = transport.readByte(); // bits 7-4 for x, bits 3-0 for y + int subtileX = dimensions >> 4 & 0x0f; + int subtileY = dimensions & 0x0f; + dimensions = transport.readByte(); // bits 7-4 for w, bits 3-0 for h + int subtileWidth = 1 + (dimensions >> 4 & 0x0f); + int subtileHeight = 1 + (dimensions & 0x0f); + renderer.fillRect(color, + tileX + subtileX, tileY + subtileY, + subtileWidth, subtileHeight); + } + } + + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RREDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RREDecoder.java new file mode 100644 index 0000000..6b1c2a4 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RREDecoder.java @@ -0,0 +1,49 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +public class RREDecoder extends Decoder { + + @Override + public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + int numOfSubrectangles = transport.readInt32(); + int color = renderer.readPixelColor(transport); + renderer.fillRect(color, rect); + for (int i = 0; i < numOfSubrectangles; ++i) { + color = renderer.readPixelColor(transport); + int x = transport.readUInt16(); + int y = transport.readUInt16(); + int width = transport.readUInt16(); + int height = transport.readUInt16(); + renderer.fillRect(color, rect.x + x, rect.y + y, width, height); + } + + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RawDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RawDecoder.java new file mode 100644 index 0000000..d7d0075 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RawDecoder.java @@ -0,0 +1,51 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +public class RawDecoder extends Decoder { + private static RawDecoder instance = new RawDecoder(); + public static RawDecoder getInstance() { + return instance; + } + private RawDecoder() { /*empty*/ } + + @Override + public void decode(Transport transport, + Renderer renderer, FramebufferUpdateRectangle rect) throws TransportException { + decode(transport, renderer, rect.x, rect.y, rect.width, rect.height); + } + + public void decode(Transport transport, Renderer renderer, int x, int y, + int width, int height) throws TransportException { + int length = width * height * renderer.getBytesPerPixel(); + byte [] bytes = ByteBuffer.getInstance().getBuffer(length); + transport.readBytes(bytes, 0, length); + renderer.drawBytes(bytes, x, y, width, height); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RichCursorDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RichCursorDecoder.java new file mode 100644 index 0000000..035506f --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/RichCursorDecoder.java @@ -0,0 +1,73 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * Decoder for RichCursor pseudo encoding + */ +public class RichCursorDecoder extends Decoder { + + @Override + public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + int bytesPerPixel = renderer.getBytesPerPixel(); + int length = rect.width * rect.height * bytesPerPixel; + if (0 == length) + return; + byte[] buffer = ByteBuffer.getInstance().getBuffer(length); + transport.readBytes(buffer, 0, length); + + StringBuilder sb = new StringBuilder(" "); + for (int i=0; i 0; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/TightDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/TightDecoder.java new file mode 100644 index 0000000..62e51d2 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/TightDecoder.java @@ -0,0 +1,291 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.ColorDecoder; +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +import java.util.logging.Logger; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * Tight protocol extention decoder + */ +public class TightDecoder extends Decoder { + private static Logger logger = Logger.getLogger("com.glavsoft.rfb.encoding.decoder"); + + private static final int FILL_TYPE = 0x08; + private static final int JPEG_TYPE = 0x09; + + private static final int FILTER_ID_MASK = 0x40; + private static final int STREAM_ID_MASK = 0x30; + + private static final int BASIC_FILTER = 0x00; + private static final int PALETTE_FILTER = 0x01; + private static final int GRADIENT_FILTER = 0x02; + private static final int MIN_SIZE_TO_COMPRESS = 12; + + static final int DECODERS_NUM = 4; + Inflater[] decoders; + + private int decoderId; + private int[] palette; + + public TightDecoder() { + reset(); + } + + @Override + public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + int bytesPerPixel = renderer.getBytesPerPixelTight(); + + /** + * bits + * 7 - FILL or JPEG type + * 6 - filter presence flag + * 5, 4 - decoder to use when Basic type (bit 7 not set) + * or + * 4 - JPEG type when set bit 7 + * 3 - reset decoder #3 + * 2 - reset decoder #2 + * 1 - reset decoder #1 + * 0 - reset decoder #0 + */ + int compControl = transport.readUInt8(); + resetDecoders(compControl); + + int compType = compControl >> 4 & 0x0F; + switch (compType) { + case FILL_TYPE: + int color = renderer.readTightPixelColor(transport); + renderer.fillRect(color, rect); + break; + case JPEG_TYPE: + assert 3 == bytesPerPixel : "Tight doesn't support JPEG subencoding while depth not equal to 24bpp is used"; + processJpegType(transport, renderer, rect); + break; + default: + assert compType <= JPEG_TYPE : "Compression control byte is incorrect!"; + processBasicType(compControl, transport, renderer, rect); + } + } + + private void processBasicType(int compControl, Transport transport, + Renderer renderer, FramebufferUpdateRectangle rect) throws TransportException { + decoderId = (compControl & STREAM_ID_MASK) >> 4; + + int filterId = 0; + if ((compControl & FILTER_ID_MASK) > 0) { // filter byte presence + filterId = transport.readUInt8(); + } + int bytesPerCPixel = renderer.getBytesPerPixelTight(); + int lengthCurrentbpp = bytesPerCPixel * rect.width * rect.height; + byte [] buffer; + switch (filterId) { + case BASIC_FILTER: + buffer = readTightData(lengthCurrentbpp, transport); + renderer.drawTightBytes(buffer, 0, rect.x, rect.y, rect.width, rect.height); + break; + case PALETTE_FILTER: + int paletteSize = transport.readUInt8() + 1; + completePalette(paletteSize, transport, renderer); + int dataLength = paletteSize == 2 ? + rect.height * ((rect.width + 7) / 8) : + rect.width * rect.height; + buffer = readTightData(dataLength, transport); + renderer.drawBytesWithPalette(buffer, rect, palette, paletteSize); + break; + case GRADIENT_FILTER: +/* + * The "gradient" filter pre-processes pixel data with a simple algorithm + * which converts each color component to a difference between a "predicted" + * intensity and the actual intensity. Such a technique does not affect + * uncompressed data size, but helps to compress photo-like images better. + * Pseudo-code for converting intensities to differences is the following: + * + * P[i,j] := V[i-1,j] + V[i,j-1] - V[i-1,j-1]; + * if (P[i,j] < 0) then P[i,j] := 0; + * if (P[i,j] > MAX) then P[i,j] := MAX; + * D[i,j] := V[i,j] - P[i,j]; + * + * Here V[i,j] is the intensity of a color component for a pixel at + * coordinates (i,j). MAX is the maximum value of intensity for a color + * component.*/ + buffer = readTightData(bytesPerCPixel * rect.width * rect.height, transport); + byte [][] opRows = new byte[2][rect.width * 3 + 3]; + int opRowIndex = 0; + byte [] components = new byte[3]; + int pixelOffset = 0; + ColorDecoder colorDecoder = renderer.getColorDecoder(); + for (int i = 0; i < rect.height; ++i) { + // exchange thisRow and prevRow: + byte [] thisRow = opRows[opRowIndex]; + byte [] prevRow = opRows[opRowIndex = (opRowIndex + 1) % 2]; + for (int j = 3; j < rect.width * 3 + 3; j += 3) { + colorDecoder.fillRawComponents(components, buffer, pixelOffset); + pixelOffset += bytesPerCPixel; + int + d = (0xff & prevRow[j + 0]) + // "upper" pixel (from prev row) + (0xff & thisRow[j + 0 - 3]) - // prev pixel + (0xff & prevRow[j + 0 - 3]); // "diagonal" prev pixel + thisRow[j + 0] = (byte) (components[0] + (d < 0 ? 0 : d > colorDecoder.redMax ? colorDecoder.redMax: d) & colorDecoder.redMax); + d = (0xff & prevRow[j + 1]) + + (0xff & thisRow[j + 1 - 3]) - + (0xff & prevRow[j + 1 - 3]); + thisRow[j + 1] = (byte) (components[1] + (d < 0 ? 0 : d > colorDecoder.greenMax ? colorDecoder.greenMax: d) & colorDecoder.greenMax); + d = (0xff & prevRow[j + 2]) + + (0xff & thisRow[j + 2 - 3]) - + (0xff & prevRow[j + 2 - 3]); + thisRow[j + 2] = (byte) (components[2] + (d < 0 ? 0 : d > colorDecoder.blueMax ? colorDecoder.blueMax: d) & colorDecoder.blueMax); + } + renderer.drawUncaliberedRGBLine(thisRow, rect.x, rect.y + i, rect.width); + } + + break; + default: + break; + } + } + + /** + * Complete palette from transport + */ + private void completePalette(int paletteSize, Transport transport, Renderer renderer) throws TransportException { + /** + * When bytesPerPixel == 1 && paletteSize == 2 read 2 bytes of palette + * When bytesPerPixel == 1 && paletteSize != 2 - error + * When bytesPerPixel == 3 (4) read (paletteSize * 3) bytes of palette + * so use renderer.readPixelColor + */ + if (null == palette) palette = new int[256]; + for (int i = 0; i < paletteSize; ++i) { + palette[i] = renderer.readTightPixelColor(transport); + } + } + + /** + * Reads compressed (expected length >= MIN_SIZE_TO_COMPRESS) or + * uncompressed data. When compressed decompresses it. + * + * @param expectedLength expected data length in bytes + * @param transport data source + * @return result data + * @throws TransportException + */ + private byte[] readTightData(int expectedLength, Transport transport) throws TransportException { + if (expectedLength < MIN_SIZE_TO_COMPRESS) { + byte [] buffer = ByteBuffer.getInstance().getBuffer(expectedLength); + transport.readBytes(buffer, 0, expectedLength); + return buffer; + } else + return readCompressedData(expectedLength, transport); + } + + /** + * Reads compressed data length, then read compressed data into rawBuffer + * and decompress data with expected length == length + * + * Note: returned data contains not only decompressed data but raw data at array tail + * which need to be ignored. Use only first expectedLength bytes. + * + * @param expectedLength expected data length + * @param transport data source + * @return decompressed data (length == expectedLength) / + followed raw data (ignore, please) + * @throws TransportException + */ + private byte[] readCompressedData(int expectedLength, Transport transport) throws TransportException { + int rawDataLength = readCompactSize(transport); + + byte [] buffer = ByteBuffer.getInstance().getBuffer(expectedLength + rawDataLength); + // read compressed (raw) data behind space allocated for decompressed data + transport.readBytes(buffer, expectedLength, rawDataLength); + if (null == decoders[decoderId]) { + decoders[decoderId] = new Inflater(); + } + Inflater decoder = decoders[decoderId]; + decoder.setInput(buffer, expectedLength, rawDataLength); + try { + decoder.inflate(buffer, 0, expectedLength); + } catch (DataFormatException e) { + logger.throwing("TightDecoder", "readCompressedData", e); + throw new TransportException("cannot inflate tight compressed data", e); + } + return buffer; + } + + private void processJpegType(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + int jpegBufferLength = readCompactSize(transport); + byte [] bytes = ByteBuffer.getInstance().getBuffer(jpegBufferLength); + transport.readBytes(bytes, 0, jpegBufferLength); + renderer.drawJpegImage(bytes, 0, jpegBufferLength, rect); + } + + /** + * Read an integer from transport in compact representation (from 1 to 3 bytes). + * Highest bit of read byte set to 1 means next byte contains data. + * Lower 7 bit of each byte contains significant data. Max bytes = 3. + * Less significant bytes first order. + * + * @param transport data source + * @return int value + * @throws TransportException + */ + private int readCompactSize(Transport transport) throws TransportException { + int b = transport.readUInt8(); + int size = b & 0x7F; + if ((b & 0x80) != 0) { + b = transport.readUInt8(); + size += (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + size += transport.readUInt8() << 14; + } + } + return size; + } + + /** + * Flush (reset) zlib decoders when bits 3, 2, 1, 0 of compControl is set + * @param compControl control flags + */ + private void resetDecoders(int compControl) { + for (int i=0; i < DECODERS_NUM; ++i) { + if ((compControl & 1) != 0 && decoders[i] != null) { + decoders[i].reset(); + } + compControl >>= 1; + } + + } + + @Override + public void reset() { + decoders = new Inflater[DECODERS_NUM]; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ZRLEDecoder.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ZRLEDecoder.java new file mode 100644 index 0000000..aa908b0 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/encoding/decoder/ZRLEDecoder.java @@ -0,0 +1,166 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.encoding.decoder; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +public class ZRLEDecoder extends ZlibDecoder { + private static final int MAX_TILE_SIZE = 64; + private int[] decodedBitmap; + private int[] palette; + + @Override + public void decode(Transport transport, Renderer renderer, + FramebufferUpdateRectangle rect) throws TransportException { + int zippedLength = (int) transport.readUInt32(); + if (0 == zippedLength) return; + int length = rect.width * rect.height * renderer.getBytesPerCPixel() + + (rect.width / MAX_TILE_SIZE + 1) * (rect.height / MAX_TILE_SIZE + 1); + byte[] bytes = unzip(transport, zippedLength, length); + int offset = zippedLength; + int maxX = rect.x + rect.width; + int maxY = rect.y + rect.height; + if (null == palette) { + palette = new int [128]; + } + if (null == decodedBitmap) { + decodedBitmap = new int[MAX_TILE_SIZE * MAX_TILE_SIZE]; + } + for (int tileY = rect.y; tileY < maxY; tileY += MAX_TILE_SIZE) { + int tileHeight = Math.min(maxY - tileY, MAX_TILE_SIZE); + + for (int tileX = rect.x; tileX < maxX; tileX += MAX_TILE_SIZE) { + int tileWidth = Math.min(maxX - tileX, MAX_TILE_SIZE); + int subencoding = bytes[offset++] & 0x0ff; + // 128 -plain RLE, 130-255 - Palette RLE + boolean isRle = (subencoding & 128) != 0; + // 2 to 16 for raw packed palette data, 130 to 255 for Palette RLE (subencoding - 128) + int paletteSize = subencoding & 127; + offset += readPalette(bytes, offset, renderer, paletteSize); + if (1 == subencoding) { // A solid tile consisting of a single colour + renderer.fillRect(palette[0], tileX, tileY, tileWidth, tileHeight); + continue; + } + if (isRle) { + if (0 == paletteSize) { // subencoding == 128 (or paletteSize == 0) - Plain RLE + offset += decodePlainRle(bytes, offset, renderer, tileX, tileY, tileWidth, tileHeight); + } else { + offset += decodePaletteRle(bytes, offset, renderer, tileX, tileY, tileWidth, tileHeight); + } + } else { + if (0 == paletteSize) { // subencoding == 0 (or paletteSize == 0) - raw CPIXEL data + offset += decodeRaw(bytes, offset, renderer, tileX, tileY, tileWidth, tileHeight); + } else { + offset += decodePacked(bytes, offset, renderer, paletteSize, tileX, tileY, tileWidth, tileHeight); + } + } + } + } + } + + private int decodePlainRle(byte[] bytes, int offset, Renderer renderer, + int tileX, int tileY, int tileWidth, int tileHeight) { + int bytesPerCPixel = renderer.getBytesPerCPixel(); + int decodedOffset = 0; + int decodedEnd = tileWidth * tileHeight; + int index = offset; + while (decodedOffset < decodedEnd) { + int color = renderer.getCompactPixelColor(bytes, index); + index += bytesPerCPixel; + int rlength = 1; + do { + rlength += bytes[index] & 0x0ff; + } while ((bytes[index++] & 0x0ff) == 255); + assert rlength <= decodedEnd - decodedOffset; + renderer.fillColorBitmapWithColor(decodedBitmap, decodedOffset, rlength, color); + decodedOffset += rlength; + } + renderer.drawColoredBitmap(decodedBitmap, tileX, tileY, tileWidth, tileHeight); + return index - offset; + } + + private int decodePaletteRle(byte[] bytes, int offset, Renderer renderer, + int tileX, int tileY, int tileWidth, int tileHeight) { + int decodedOffset = 0; + int decodedEnd = tileWidth * tileHeight; + int index = offset; + while (decodedOffset < decodedEnd) { + int colorIndex = bytes[index++]; + int color = palette[colorIndex & 127]; + int rlength = 1; + if ((colorIndex & 128) != 0) { + do { + rlength += bytes[index] & 0x0ff; + } while (bytes[index++] == (byte) 255); + } + assert rlength <= decodedEnd - decodedOffset; + renderer.fillColorBitmapWithColor(decodedBitmap, decodedOffset, rlength, color); + decodedOffset += rlength; + } + renderer.drawColoredBitmap(decodedBitmap, tileX, tileY, tileWidth, tileHeight); + return index - offset; + } + + private int decodePacked(byte[] bytes, int offset, Renderer renderer, + int paletteSize, int tileX, int tileY, int tileWidth, int tileHeight) { + int bitsPerPalletedPixel = paletteSize > 16 ? 8 : paletteSize > 4 ? 4 : paletteSize > 2 ? 2 : 1; + int packedOffset = offset; + int decodedOffset = 0; + for (int i = 0; i < tileHeight; ++i) { + int decodedRowEnd = decodedOffset + tileWidth; + int byteProcessed = 0; + int bitsRemain = 0; + + while (decodedOffset < decodedRowEnd) { + if (bitsRemain == 0) { + byteProcessed = bytes[packedOffset++]; + bitsRemain = 8; + } + bitsRemain -= bitsPerPalletedPixel; + int index = byteProcessed >> bitsRemain & (1 << bitsPerPalletedPixel) - 1 & 127; + int color = palette[index]; + renderer.fillColorBitmapWithColor(decodedBitmap, decodedOffset, 1, color); + ++decodedOffset; + } + } + renderer.drawColoredBitmap(decodedBitmap, tileX, tileY, tileWidth, tileHeight); + return packedOffset - offset; + } + + private int decodeRaw(byte[] bytes, int offset, Renderer renderer, + int tileX, int tileY, int tileWidth, int tileHeight) throws TransportException { + return renderer.drawCompactBytes(bytes, offset, tileX, tileY, tileWidth, tileHeight); + } + + private int readPalette(byte[] bytes, int offset, Renderer renderer, int paletteSize) { + final int bytesPerCPixel = renderer.getBytesPerCPixel(); + for (int i=0; i queue; + + public MessageQueue() { + queue = new LinkedBlockingQueue(); + } + + public void put(ClientToServerMessage message) { + if ( ! queue.offer(message)) { + Logger.getLogger(getClass().getName()).severe("Cannot put message into message queue. Skip: " + message); + } + } + + /** + * Retrieves and removes the head of this queue, waiting if necessary until an element becomes available. + * Retrieves and removes the head of this queue, waiting up to the certain wait time if necessary for + * an element to become available. + * @return the head of this queue, or null if the specified waiting time elapses before an element is available + * @throws InterruptedException - if interrupted while waiting + */ + public ClientToServerMessage get() throws InterruptedException { + return queue.poll(1, TimeUnit.SECONDS); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/Protocol.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/Protocol.java new file mode 100644 index 0000000..1468844 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/Protocol.java @@ -0,0 +1,473 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol; + +import com.glavsoft.core.SettingsChangedEvent; +import com.glavsoft.exceptions.AuthenticationFailedException; +import com.glavsoft.exceptions.FatalException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.exceptions.UnsupportedProtocolVersionException; +import com.glavsoft.exceptions.UnsupportedSecurityTypeException; +import com.glavsoft.rfb.ClipboardController; +import com.glavsoft.rfb.IChangeSettingsListener; +import com.glavsoft.rfb.IRepaintController; +import com.glavsoft.rfb.IRequestString; +import com.glavsoft.rfb.IRfbSessionListener; +import com.glavsoft.rfb.RfbCapabilityInfo; +import com.glavsoft.rfb.client.ClientMessageType; +import com.glavsoft.rfb.client.ClientToServerMessage; +import com.glavsoft.rfb.client.FramebufferUpdateRequestMessage; +import com.glavsoft.rfb.client.SetEncodingsMessage; +import com.glavsoft.rfb.client.SetPixelFormatMessage; +import com.glavsoft.rfb.encoding.EncodingType; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.rfb.encoding.decoder.ByteBuffer; +import com.glavsoft.rfb.encoding.decoder.CopyRectDecoder; +import com.glavsoft.rfb.encoding.decoder.CursorPosDecoder; +import com.glavsoft.rfb.encoding.decoder.Decoder; +import com.glavsoft.rfb.encoding.decoder.DesctopSizeDecoder; +import com.glavsoft.rfb.encoding.decoder.HextileDecoder; +import com.glavsoft.rfb.encoding.decoder.RREDecoder; +import com.glavsoft.rfb.encoding.decoder.RawDecoder; +import com.glavsoft.rfb.encoding.decoder.RichCursorDecoder; +import com.glavsoft.rfb.encoding.decoder.TightDecoder; +import com.glavsoft.rfb.encoding.decoder.ZRLEDecoder; +import com.glavsoft.rfb.encoding.decoder.ZlibDecoder; +import com.glavsoft.rfb.protocol.handlers.Handshaker; +import com.glavsoft.rfb.protocol.tunnel.TunnelType; +import com.glavsoft.transport.BaudrateMeter; +import com.glavsoft.transport.Transport; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +public class Protocol implements IChangeSettingsListener { + private final ProtocolContext context; + private final Logger logger; + private final IRequestString passwordRetriever; + private MessageQueue messageQueue; + private SenderTask senderTask; + private ReceiverTask receiverTask; + private IRfbSessionListener rfbSessionListener; + private IRepaintController repaintController; + private Thread senderThread; + private Thread receiverThread; + private PixelFormat serverPixelFormat; + + private final Map decoders = new LinkedHashMap(); + private final Set clientMessageTypes = new HashSet(); + private boolean inCleanUp = false; + private boolean isMac; + private BaudrateMeter baudrateMeter; + private IRequestString connectionIdRetriever; + + public Protocol(Transport transport, IRequestString passwordRetriever, ProtocolSettings settings) { + context = new ProtocolContext(); + context.transport = transport; + this.passwordRetriever = passwordRetriever; + logger = Logger.getLogger(getClass().getName()); + context.settings = settings; + decoders.put(EncodingType.RAW_ENCODING, RawDecoder.getInstance()); + } + + public void handshake() throws UnsupportedProtocolVersionException, UnsupportedSecurityTypeException, + AuthenticationFailedException, TransportException, FatalException { + context.transport = new Handshaker(this).handshake(getTransport()); + messageQueue = new MessageQueue(); // TODO Why here? + } + + public IRequestString getPasswordRetriever() { + return passwordRetriever; + } + + /** + * Following the server initialisation message it's up to the client to send + * whichever protocol messages it wants. Typically it will send a + * SetPixelFormat message and a SetEncodings message, followed by a + * FramebufferUpdateRequest. From then on the server will send + * FramebufferUpdate messages in response to the client's + * FramebufferUpdateRequest messages. The client should send + * FramebufferUpdateRequest messages with incremental set to true when it has + * finished processing one FramebufferUpdate and is ready to process another. + * With a fast client, the rate at which FramebufferUpdateRequests are sent + * should be regulated to avoid hogging the network. + */ + public void startNormalHandling(IRfbSessionListener rfbSessionListener, + IRepaintController repaintController, ClipboardController clipboardController) { + this.rfbSessionListener = rfbSessionListener; + this.repaintController = repaintController; +// if (settings.getColorDepth() == 0) { +// settings.setColorDepth(pixelFormat.depth); // the same the server sent when not initialized yet +// } + correctServerPixelFormat(); + context.setPixelFormat(createPixelFormat(context.settings)); + sendMessage(new SetPixelFormatMessage(context.pixelFormat)); + logger.fine("sent: " + context.pixelFormat); + + sendSupportedEncodingsMessage(context.settings); + context.settings.addListener(Protocol.this); // to support pixel format (color depth), and encodings changes + context.settings.addListener(repaintController); + + sendRefreshMessage(); + senderTask = new SenderTask(messageQueue, context.transport, Protocol.this); + senderThread = new Thread(senderTask, "RfbSenderTask"); + senderThread.start(); + resetDecoders(); + receiverTask = new ReceiverTask( + context.transport, repaintController, + clipboardController, + Protocol.this, baudrateMeter); + receiverThread = new Thread(receiverTask, "RfbReceiverTask"); + receiverThread.start(); + } + + private void correctServerPixelFormat() { + // correct true color flag + if (0 == serverPixelFormat.trueColourFlag) { + //we don't support color maps, so always set true color flag up + //and select closest convenient value for bpp/depth + int depth = serverPixelFormat.depth; + if (0 == depth) depth = serverPixelFormat.bitsPerPixel; + if (0 == depth) depth = 24; + if (depth <= 3) serverPixelFormat = PixelFormat.create3bitColorDepthPixelFormat(serverPixelFormat.bigEndianFlag); + else if (depth <= 6) serverPixelFormat = PixelFormat.create6bitColorDepthPixelFormat(serverPixelFormat.bigEndianFlag); + else if (depth <= 8) serverPixelFormat = PixelFormat.create8bitColorDepthBGRPixelFormat(serverPixelFormat.bigEndianFlag); + else if (depth <= 16) serverPixelFormat = PixelFormat.create16bitColorDepthPixelFormat(serverPixelFormat.bigEndianFlag); + else serverPixelFormat = PixelFormat.create24bitColorDepthPixelFormat(serverPixelFormat.bigEndianFlag); + } + // correct .depth to use actual depth 24 instead of incorrect 32, used by ex. UltraVNC server, that cause + // protocol incompatibility in ZRLE encoding + final long significant = serverPixelFormat.redMax << serverPixelFormat.redShift | + serverPixelFormat.greenMax << serverPixelFormat.greenShift | + serverPixelFormat.blueMax << serverPixelFormat.blueShift; + if (32 == serverPixelFormat.bitsPerPixel && + ((significant & 0x00ff000000L) == 0 || (significant & 0x000000ffL) == 0) && + 32 == serverPixelFormat.depth) { + serverPixelFormat.depth = 24; + } + } + + public void sendMessage(ClientToServerMessage message) { + messageQueue.put(message); + } + + public void sendSupportedEncodingsMessage(ProtocolSettings settings) { + final LinkedHashSet encodings = new LinkedHashSet(); + final EncodingType preferredEncoding = settings.getPreferredEncoding(); + if (preferredEncoding != EncodingType.RAW_ENCODING) { + encodings.add(preferredEncoding); // preferred first + } + for (final EncodingType e : decoders.keySet()) { + if (e == preferredEncoding) continue; + switch (e) { + case RAW_ENCODING: break; + case COMPRESS_LEVEL_0 : + final int compressionLevel = settings.getCompressionLevel(); + if (compressionLevel > 0 && compressionLevel < 10) { + encodings.add(EncodingType.byId(EncodingType.COMPRESS_LEVEL_0.getId() + compressionLevel)); + } + break; + case JPEG_QUALITY_LEVEL_0 : + final int jpegQuality = settings.getJpegQuality(); + final int colorDepth = settings.getColorDepth(); + if (jpegQuality > 0 && jpegQuality < 10 && + (colorDepth == ProtocolSettings.COLOR_DEPTH_24 || + colorDepth == ProtocolSettings.COLOR_DEPTH_SERVER_SETTINGS)) { + encodings.add(EncodingType.byId(EncodingType.JPEG_QUALITY_LEVEL_0.getId() + jpegQuality)); + } + break; + case COPY_RECT: + if (settings.isAllowCopyRect()) { + encodings.add(EncodingType.COPY_RECT); + } + break; + case RICH_CURSOR: + if (settings.getMouseCursorTrack() == LocalPointer.HIDE || + settings.getMouseCursorTrack() == LocalPointer.ON) { + encodings.add(EncodingType.RICH_CURSOR); + } + break; + case CURSOR_POS: + if (settings.getMouseCursorTrack() == LocalPointer.HIDE || + settings.getMouseCursorTrack() == LocalPointer.ON) { + encodings.add(EncodingType.CURSOR_POS); + } + break; + default: + encodings.add(e); + } + } + SetEncodingsMessage encodingsMessage = new SetEncodingsMessage(encodings); + sendMessage(encodingsMessage); + logger.fine("sent: " + encodingsMessage.toString()); + } + + /** + * create pixel format by bpp + */ + private PixelFormat createPixelFormat(ProtocolSettings settings) { + int serverBigEndianFlag = serverPixelFormat.bigEndianFlag; + switch (settings.getColorDepth()) { + case ProtocolSettings.COLOR_DEPTH_24: + return PixelFormat.create24bitColorDepthPixelFormat(serverBigEndianFlag); + case ProtocolSettings.COLOR_DEPTH_16: + return PixelFormat.create16bitColorDepthPixelFormat(serverBigEndianFlag); + case ProtocolSettings.COLOR_DEPTH_8: + return hackForMacOsXScreenSharingServer(PixelFormat.create8bitColorDepthBGRPixelFormat(serverBigEndianFlag)); + case ProtocolSettings.COLOR_DEPTH_6: + return hackForMacOsXScreenSharingServer(PixelFormat.create6bitColorDepthPixelFormat(serverBigEndianFlag)); + case ProtocolSettings.COLOR_DEPTH_3: + return hackForMacOsXScreenSharingServer(PixelFormat.create3bitColorDepthPixelFormat(serverBigEndianFlag)); + case ProtocolSettings.COLOR_DEPTH_SERVER_SETTINGS: + return serverPixelFormat; + default: + // unsupported bpp, use default + return PixelFormat.create24bitColorDepthPixelFormat(serverBigEndianFlag); + } + } + + private PixelFormat hackForMacOsXScreenSharingServer(PixelFormat pixelFormat) { + if (isMac) { + pixelFormat.bitsPerPixel = pixelFormat.depth = 16; + } + return pixelFormat; + } + + @Override + public void settingsChanged(SettingsChangedEvent e) { + ProtocolSettings settings = (ProtocolSettings) e.getSource(); + if (settings.isChangedEncodings()) { + sendSupportedEncodingsMessage(settings); + } + if (settings.isChangedColorDepth() && receiverTask != null) { + receiverTask.queueUpdatePixelFormat(createPixelFormat(settings)); + } + } + + public void sendRefreshMessage() { + sendMessage(new FramebufferUpdateRequestMessage(0, 0, context.fbWidth, context.fbHeight, false)); + logger.fine("sent: full FB Refresh"); + } + + public void sendFbUpdateMessage() { + sendMessage(receiverTask.fullscreenFbUpdateIncrementalRequest); + } + + public void cleanUpSession(String message) { + cleanUpSession(); + rfbSessionListener.rfbSessionStopped(message); + } + + public void cleanUpSession() { + synchronized (this) { + if (inCleanUp) return; + inCleanUp = true; + } + if (senderTask != null && senderThread.isAlive()) { senderThread.interrupt(); } + if (receiverTask != null && receiverThread.isAlive()) { receiverThread.interrupt(); } + if (senderTask != null) { + try { + senderThread.join(1000); + } catch (InterruptedException e) { + // nop + } + senderTask = null; + } + if (receiverTask != null) { + try { + receiverThread.join(1000); + } catch (InterruptedException e) { + // nop + } + receiverTask = null; + } + synchronized (this) { + inCleanUp = false; + } + ByteBuffer.removeInstance(); + } + + public void setServerPixelFormat(PixelFormat serverPixelFormat) { + this.serverPixelFormat = serverPixelFormat; + } + + public ProtocolSettings getSettings() { + return context.getSettings(); + } + + public Transport getTransport() { + return context.getTransport(); + } + + public int getFbWidth() { + return context.getFbWidth(); + } + + public void setFbWidth(int frameBufferWidth) { + context.setFbWidth(frameBufferWidth); + } + + public int getFbHeight() { + return context.getFbHeight(); + } + + public void setFbHeight(int frameBufferHeight) { + context.setFbHeight(frameBufferHeight); + } + + public PixelFormat getPixelFormat() { + return context.getPixelFormat(); + } + + public void setPixelFormat(PixelFormat pixelFormat) { + context.setPixelFormat(pixelFormat); + if (repaintController != null) { + repaintController.setPixelFormat(pixelFormat); + } + } + + public void setRemoteDesktopName(String name) { + context.setRemoteDesktopName(name); + } + + public String getRemoteDesktopName() { + return context.getRemoteDesktopName(); + } + + public void setTight(boolean isTight) { + context.setTight(isTight); + } + + public boolean isTight() { + return context.isTight(); + } + + public void setProtocolVersion(Handshaker.ProtocolVersion protocolVersion) { + context.setProtocolVersion(protocolVersion); + } + + public Handshaker.ProtocolVersion getProtocolVersion() { + return context.getProtocolVersion(); + } + + public void registerRfbEncodings() { + decoders.put(EncodingType.TIGHT, new TightDecoder()); + decoders.put(EncodingType.HEXTILE, new HextileDecoder()); + decoders.put(EncodingType.ZRLE, new ZRLEDecoder()); + decoders.put(EncodingType.ZLIB, new ZlibDecoder()); + decoders.put(EncodingType.RRE, new RREDecoder()); + decoders.put(EncodingType.COPY_RECT, new CopyRectDecoder()); + + decoders.put(EncodingType.RICH_CURSOR, new RichCursorDecoder()); + decoders.put(EncodingType.DESKTOP_SIZE, new DesctopSizeDecoder()); + decoders.put(EncodingType.CURSOR_POS, new CursorPosDecoder()); + } + + public void resetDecoders() { + for (Decoder decoder : decoders.values()) { + if (decoder != null) { + decoder.reset(); + } + } + } + + public Decoder getDecoderByType(EncodingType type) { + return decoders.get(type); + } + + public void registerEncoding(RfbCapabilityInfo capInfo) { + try { + final EncodingType encodingType = EncodingType.byId(capInfo.getCode()); + if ( ! decoders.containsKey(encodingType)) { + final Decoder decoder = encodingType.klass.newInstance(); + if (decoder != null) { + decoders.put(encodingType, decoder); + logger.finer("Register encoding: " + encodingType); + } + } + } catch (IllegalArgumentException e) { + logger.finer(e.getMessage()); + } catch (InstantiationException e) { + logger.warning(e.getMessage()); + } catch (IllegalAccessException e) { + logger.warning(e.getMessage()); + } + } + + public void registerClientMessageType(RfbCapabilityInfo capInfo) { + try { + final ClientMessageType clientMessageType = ClientMessageType.byId(capInfo.getCode()); + clientMessageTypes.add(clientMessageType); + logger.finer("Register client message type: " + clientMessageType); + } catch (IllegalArgumentException e) { + logger.finer(e.getMessage()); + } + } + + /** + * Check whether server is supported for given client-to-server message + * + * @param type client-to-server message type to check for + * @return true when supported + */ + public boolean isSupported(ClientMessageType type) { + return clientMessageTypes.contains(type) || ClientMessageType.isStandardType(type ); + } + + public void setTunnelType(TunnelType tunnelType) { + context.setTunnelType(tunnelType); + } + + public TunnelType getTunnelType() { + return context.getTunnelType(); + } + + public void setMac(boolean isMac) { + this.isMac = isMac; + } + + public void setBaudrateMeter(BaudrateMeter baudrateMeter) { + this.baudrateMeter = baudrateMeter; + } + + public int kBPS() { + return baudrateMeter == null ? -1 : baudrateMeter.kBPS(); + } + + public boolean isMac() { + return isMac; + } + + public void setConnectionIdRetriever(IRequestString connectionIdRetriever) { + this.connectionIdRetriever = connectionIdRetriever; + } + + public IRequestString getConnectionIdRetriever() { + return connectionIdRetriever; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolContext.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolContext.java new file mode 100644 index 0000000..d163d5e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolContext.java @@ -0,0 +1,105 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol; + +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.rfb.protocol.handlers.Handshaker; +import com.glavsoft.rfb.protocol.tunnel.TunnelType; +import com.glavsoft.transport.Transport; + +public class ProtocolContext { + int fbWidth; + int fbHeight; + PixelFormat pixelFormat; + Transport transport; + String remoteDesktopName; + boolean isTight; + Handshaker.ProtocolVersion protocolVersion; + ProtocolSettings settings; + private TunnelType tunnelType; + + public PixelFormat getPixelFormat() { + return pixelFormat; + } + + public void setPixelFormat(PixelFormat pixelFormat) { + this.pixelFormat = pixelFormat; + } + + public String getRemoteDesktopName() { + return remoteDesktopName; + } + + public void setRemoteDesktopName(String name) { + remoteDesktopName = name; + } + + public int getFbWidth() { + return fbWidth; + } + + public void setFbWidth(int fbWidth) { + this.fbWidth = fbWidth; + } + + public int getFbHeight() { + return fbHeight; + } + + public void setFbHeight(int fbHeight) { + this.fbHeight = fbHeight; + } + + public ProtocolSettings getSettings() { + return settings; + } + + public Transport getTransport() { + return transport; + } + + public void setTight(boolean isTight) { + this.isTight = isTight; + } + + public boolean isTight() { + return isTight; + } + + public void setProtocolVersion(Handshaker.ProtocolVersion protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public Handshaker.ProtocolVersion getProtocolVersion() { + return protocolVersion; + } + + public void setTunnelType(TunnelType tunnelType) { + this.tunnelType = tunnelType; + } + + public TunnelType getTunnelType() { + return tunnelType; + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolSettings.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolSettings.java new file mode 100644 index 0000000..7eb074a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ProtocolSettings.java @@ -0,0 +1,343 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol; + +import com.glavsoft.core.SettingsChangedEvent; +import com.glavsoft.rfb.IChangeSettingsListener; +import com.glavsoft.rfb.encoding.EncodingType; +import com.glavsoft.rfb.protocol.tunnel.TunnelType; + +import java.io.Serializable; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Protocol Settings class + */ +public class ProtocolSettings implements Serializable { + private static final long serialVersionUID = 1L; + + private static final EncodingType DEFAULT_PREFERRED_ENCODING = EncodingType.TIGHT; + public static final int DEFAULT_JPEG_QUALITY = 6; + private static final int DEFAULT_COMPRESSION_LEVEL = -6; + + // color depth constants + public static final int COLOR_DEPTH_32 = 32; + public static final int COLOR_DEPTH_24 = 24; + public static final int COLOR_DEPTH_16 = 16; + public static final int COLOR_DEPTH_8 = 8; + public static final int COLOR_DEPTH_6 = 6; + public static final int COLOR_DEPTH_3 = 3; + + public static final int COLOR_DEPTH_SERVER_SETTINGS = 0; + + private static final int DEFAULT_COLOR_DEPTH = COLOR_DEPTH_24; + + public static final int CHANGED_VIEW_ONLY = 1; // 1 << 0; + public static final int CHANGED_ENCODINGS = 1 << 1; + public static final int CHANGED_ALLOW_COPY_RECT = 1 << 2; + public static final int CHANGED_SHOW_REMOTE_CURSOR = 1 << 3; + public static final int CHANGED_MOUSE_CURSOR_TRACK = 1 << 4; + public static final int CHANGED_COMPRESSION_LEVEL = 1 << 5; + public static final int CHANGED_JPEG_QUALITY = 1 << 6; + public static final int CHANGED_ALLOW_CLIPBOARD_TRANSFER = 1 << 7; + public static final int CHANGED_CONVERT_TO_ASCII = 1 << 8; + public static final int CHANGED_COLOR_DEPTH = 1 << 9; + public static final int CHANGED_SHARED = 1 << 10; + + private static final int MIN_COMPRESSION_LEVEL = 1; + private static final int MAX_COMPRESSION_LEVEL = 9; + private static final int MIN_JPEG_QUALITY = 1; + private static final int MAX_JPEG_QUALITY = 9; + + private transient int changedSettingsMask; + + private boolean sharedFlag; + private boolean viewOnly; + private EncodingType preferredEncoding; + private boolean allowCopyRect; + private boolean showRemoteCursor; + private LocalPointer mouseCursorTrack; + private int compressionLevel; + private int jpegQuality; + private boolean allowClipboardTransfer; + private boolean convertToAscii; + private int colorDepth; + + private transient final List listeners; + private transient String remoteCharsetName; + private TunnelType tunnelType; + + public static ProtocolSettings getDefaultSettings() { + return new ProtocolSettings(); + } + + private ProtocolSettings() { + sharedFlag = true; + viewOnly = false; + showRemoteCursor = true; + mouseCursorTrack = LocalPointer.ON; + preferredEncoding = DEFAULT_PREFERRED_ENCODING; + allowCopyRect = true; + compressionLevel = DEFAULT_COMPRESSION_LEVEL; + jpegQuality = DEFAULT_JPEG_QUALITY; + convertToAscii = false; + allowClipboardTransfer = true; + colorDepth = COLOR_DEPTH_SERVER_SETTINGS; + + listeners = new CopyOnWriteArrayList(); + changedSettingsMask = 0; + } + + public ProtocolSettings(ProtocolSettings s) { + this(); + copyDataFrom(s); + changedSettingsMask = s.changedSettingsMask; + } + + public void copyDataFrom(ProtocolSettings s) { + copyDataFrom(s, 0); + } + + public void copyDataFrom(ProtocolSettings s, int mask) { + if (null == s) return; + if ((mask & CHANGED_SHARED) == 0) setSharedFlag(s.sharedFlag); + if ((mask & CHANGED_VIEW_ONLY) == 0) setViewOnly(s.viewOnly); + if ((mask & CHANGED_ALLOW_COPY_RECT) == 0) setAllowCopyRect(s.allowCopyRect); + if ((mask & CHANGED_SHOW_REMOTE_CURSOR) == 0) setShowRemoteCursor(s.showRemoteCursor); + if ((mask & CHANGED_ALLOW_CLIPBOARD_TRANSFER) == 0) setAllowClipboardTransfer(s.allowClipboardTransfer); + + if ((mask & CHANGED_MOUSE_CURSOR_TRACK) == 0) setMouseCursorTrack(s.mouseCursorTrack); + if ((mask & CHANGED_COMPRESSION_LEVEL) == 0) setCompressionLevel(s.compressionLevel); + if ((mask & CHANGED_JPEG_QUALITY) == 0) setJpegQuality(s.jpegQuality); + if ((mask & CHANGED_CONVERT_TO_ASCII) == 0) setConvertToAscii(s.convertToAscii); + if ((mask & CHANGED_COLOR_DEPTH) == 0) setColorDepth(s.colorDepth); + if ((mask & CHANGED_ENCODINGS) == 0) setPreferredEncoding(s.preferredEncoding); + } + + public void addListener(IChangeSettingsListener listener) { + listeners.add(listener); + } + + public byte getSharedFlag() { + return (byte) (sharedFlag ? 1 : 0); + } + + public boolean isShared() { + return sharedFlag; + } + + public void setSharedFlag(boolean sharedFlag) { + if (this.sharedFlag != sharedFlag) { + this.sharedFlag = sharedFlag; + changedSettingsMask |= CHANGED_SHARED; + } + } + + public boolean isViewOnly() { + return viewOnly; + } + + public void setViewOnly(boolean viewOnly) { + if (this.viewOnly != viewOnly) { + this.viewOnly = viewOnly; + changedSettingsMask |= CHANGED_VIEW_ONLY; + } + } + + public int getColorDepth() { + return colorDepth; + } + + /** + * Set depth only in 3, 6, 8, 16, 32. When depth is wrong, it resets to {@link #DEFAULT_COLOR_DEPTH} + */ + public void setColorDepth(int depth) { + if (colorDepth != depth) { + changedSettingsMask |= CHANGED_COLOR_DEPTH | CHANGED_ENCODINGS; + switch (depth) { + case COLOR_DEPTH_32: + colorDepth = COLOR_DEPTH_24; + break; + case COLOR_DEPTH_24: + case COLOR_DEPTH_16: + case COLOR_DEPTH_8: + case COLOR_DEPTH_6: + case COLOR_DEPTH_3: + case COLOR_DEPTH_SERVER_SETTINGS: + colorDepth = depth; + break; + default: + colorDepth = DEFAULT_COLOR_DEPTH; + } + } + } + + public void fireListeners() { + if (null == listeners) return; + final SettingsChangedEvent event = new SettingsChangedEvent(new ProtocolSettings(this)); + changedSettingsMask = 0; + for (IChangeSettingsListener listener : listeners) { + listener.settingsChanged(event); + } + } + + public static boolean isRfbSettingsChangedFired(SettingsChangedEvent event) { + return event.getSource() instanceof ProtocolSettings; + } + + public void setPreferredEncoding(EncodingType preferredEncoding) { + if (this.preferredEncoding != preferredEncoding) { + this.preferredEncoding = preferredEncoding; + changedSettingsMask |= CHANGED_ENCODINGS; + } + } + + public EncodingType getPreferredEncoding() { + return preferredEncoding; + } + + public void setAllowCopyRect(boolean allowCopyRect) { + if (this.allowCopyRect != allowCopyRect) { + this.allowCopyRect = allowCopyRect; + changedSettingsMask |= CHANGED_ALLOW_COPY_RECT | CHANGED_ENCODINGS; + } + } + + public boolean isAllowCopyRect() { + return allowCopyRect; + } + + private void setShowRemoteCursor(boolean showRemoteCursor) { + if (this.showRemoteCursor != showRemoteCursor) { + this.showRemoteCursor = showRemoteCursor; + changedSettingsMask |= CHANGED_SHOW_REMOTE_CURSOR | CHANGED_ENCODINGS; + } + } + + public boolean isShowRemoteCursor() { + return showRemoteCursor; + } + + public void setMouseCursorTrack(LocalPointer mouseCursorTrack) { + if (this.mouseCursorTrack != mouseCursorTrack) { + this.mouseCursorTrack = mouseCursorTrack; + changedSettingsMask |= CHANGED_MOUSE_CURSOR_TRACK | CHANGED_ENCODINGS; + setShowRemoteCursor(LocalPointer.ON == mouseCursorTrack); + } + } + + public LocalPointer getMouseCursorTrack() { + return mouseCursorTrack; + } + + public int setCompressionLevel(int compressionLevel) { + if (compressionLevel >= MIN_COMPRESSION_LEVEL && compressionLevel <= MAX_COMPRESSION_LEVEL && + this.compressionLevel != compressionLevel) { + this.compressionLevel = compressionLevel; + changedSettingsMask |= CHANGED_COMPRESSION_LEVEL | CHANGED_ENCODINGS; + } + return this.compressionLevel; + } + + public int getCompressionLevel() { + return compressionLevel; + } + + public int setJpegQuality(int jpegQuality) { + if (jpegQuality >= MIN_JPEG_QUALITY && jpegQuality <= MAX_JPEG_QUALITY && + this.jpegQuality != jpegQuality) { + this.jpegQuality = jpegQuality; + changedSettingsMask |= CHANGED_JPEG_QUALITY | CHANGED_ENCODINGS; + } + return this.jpegQuality; + } + + public int getJpegQuality() { + return jpegQuality; + } + + public void setAllowClipboardTransfer(boolean enable) { + if (this.allowClipboardTransfer != enable) { + this.allowClipboardTransfer = enable; + changedSettingsMask |= CHANGED_ALLOW_CLIPBOARD_TRANSFER; + } + } + + public boolean isAllowClipboardTransfer() { + return allowClipboardTransfer; + } + + public boolean isConvertToAscii() { + return convertToAscii; + } + + public void setConvertToAscii(boolean convertToAscii) { + if (this.convertToAscii != convertToAscii) { + this.convertToAscii = convertToAscii; + changedSettingsMask |= CHANGED_CONVERT_TO_ASCII; + } + } + + public boolean isChangedEncodings() { + return (changedSettingsMask & CHANGED_ENCODINGS) == CHANGED_ENCODINGS; + } + + public boolean isChangedColorDepth() { + return (changedSettingsMask & CHANGED_COLOR_DEPTH) == CHANGED_COLOR_DEPTH; + } + + public void setRemoteCharsetName(String remoteCharsetName) { + this.remoteCharsetName = remoteCharsetName; + } + + public String getRemoteCharsetName() { + return remoteCharsetName; + } + + @Override + public String toString() { + return "ProtocolSettings{" + + "sharedFlag=" + sharedFlag + + ", viewOnly=" + viewOnly + + ", preferredEncoding=" + preferredEncoding + + ", allowCopyRect=" + allowCopyRect + + ", showRemoteCursor=" + showRemoteCursor + + ", mouseCursorTrack=" + mouseCursorTrack + + ", compressionLevel=" + compressionLevel + + ", jpegQuality=" + jpegQuality + + ", allowClipboardTransfer=" + allowClipboardTransfer + + ", convertToAscii=" + convertToAscii + + ", colorDepth=" + colorDepth + + '}'; + } + + public TunnelType getTunnelType() { + return tunnelType; + } + + public void setTunnelType(TunnelType tunnelType) { + this.tunnelType = tunnelType; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ReceiverTask.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ReceiverTask.java new file mode 100644 index 0000000..76d098b --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/ReceiverTask.java @@ -0,0 +1,204 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.exceptions.CommonException; +import com.glavsoft.exceptions.ProtocolException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.ClipboardController; +import com.glavsoft.rfb.IRepaintController; +import com.glavsoft.rfb.client.FramebufferUpdateRequestMessage; +import com.glavsoft.rfb.client.SetPixelFormatMessage; +import com.glavsoft.rfb.encoding.EncodingType; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.rfb.encoding.decoder.Decoder; +import com.glavsoft.rfb.encoding.decoder.FramebufferUpdateRectangle; +import com.glavsoft.transport.BaudrateMeter; +import com.glavsoft.transport.Transport; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.Logger; + +public class ReceiverTask implements Runnable { + private static final byte FRAMEBUFFER_UPDATE = 0; + private static final byte SET_COLOR_MAP_ENTRIES = 1; + private static final byte BELL = 2; + private static final byte SERVER_CUT_TEXT = 3; + + + private static Logger logger = Logger.getLogger("com.glavsoft.rfb.protocol.ReceiverTask"); + private final Transport transport; + private Renderer renderer; + private final IRepaintController repaintController; + private final ClipboardController clipboardController; + protected FramebufferUpdateRequestMessage fullscreenFbUpdateIncrementalRequest; + private final Protocol protocol; + private BaudrateMeter baudrateMeter; + private PixelFormat pixelFormat; + private volatile boolean needSendPixelFormat; + + public ReceiverTask(Transport transport, + IRepaintController repaintController, ClipboardController clipboardController, + Protocol protocol, BaudrateMeter baudrateMeter) { + this.transport = transport; + this.repaintController = repaintController; + this.clipboardController = clipboardController; + this.protocol = protocol; + this.baudrateMeter = baudrateMeter; + renderer = repaintController.createRenderer(transport, protocol.getFbWidth(), protocol.getFbHeight(), + protocol.getPixelFormat()); + fullscreenFbUpdateIncrementalRequest = + new FramebufferUpdateRequestMessage(0, 0, protocol.getFbWidth(), protocol.getFbHeight(), true); + } + + @Override + public void run() { + try { + while ( ! Thread.currentThread().isInterrupted()) { + byte messageId = transport.readByte(); + switch (messageId) { + case FRAMEBUFFER_UPDATE: +// logger.fine("Server message: FramebufferUpdate (0)"); + framebufferUpdateMessage(); + break; + case SET_COLOR_MAP_ENTRIES: + logger.severe("Server message SetColorMapEntries is not implemented. Skip."); + setColorMapEntries(); + break; + case BELL: + logger.fine("Server message: Bell"); + System.out.print("\0007"); + System.out.flush(); + break; + case SERVER_CUT_TEXT: + logger.fine("Server message: CutText (3)"); + serverCutText(); + break; + default: + logger.severe("Unsupported server message. Id = " + messageId); + } + } + } catch (TransportException e) { + logger.severe("Close session: " + e.getMessage()); + protocol.cleanUpSession("Connection closed."); + } catch (ProtocolException e) { + logger.severe(e.getMessage()); + protocol.cleanUpSession(e.getMessage() + "\nConnection closed."); + } catch (CommonException e) { + logger.severe(e.getMessage()); + protocol.cleanUpSession("Connection closed.."); + } catch (Throwable te) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + te.printStackTrace(pw); + protocol.cleanUpSession(te.getMessage() + "\n" + sw.toString()); + } + Logger.getLogger(getClass().getName()).finer("Receiver task stopped"); + } + + private void setColorMapEntries() throws TransportException { + transport.readByte(); // padding + transport.readUInt16(); // first color index + int length = transport.readUInt16(); + while (length-- > 0) { + transport.readUInt16(); // R + transport.readUInt16(); // G + transport.readUInt16(); // B + } + } + + private void serverCutText() throws TransportException, IOException { + transport.readByte(); // padding + transport.readInt16(); // padding + long length = transport.readInt32(); + if (0 == length) return; + if (length > Integer.MAX_VALUE) { + clipboardController.updateSystemClipboard(transport.readBytes(Integer.MAX_VALUE)); + clipboardController.updateSystemClipboard(transport.readBytes((int) (length - Integer.MAX_VALUE))); + } else { + clipboardController.updateSystemClipboard(transport.readBytes((int) length)); + } + } + + public void framebufferUpdateMessage() throws CommonException { + transport.skip(1); // padding + int numberOfRectangles = transport.readUInt16(); + while (numberOfRectangles-- > 0) { + FramebufferUpdateRectangle rect = new FramebufferUpdateRectangle(); + rect.fill(transport); + + Decoder decoder = protocol.getDecoderByType(rect.getEncodingType()); +// logger.finer(rect.toString() + (0 == numberOfRectangles ? "\n---" : "")); + if (decoder != null) { + try { + if (baudrateMeter != null) baudrateMeter.startMeasuringCycle(); + decoder.decode(transport, renderer, rect); + } finally { + if (baudrateMeter != null) baudrateMeter.stopMeasuringCycle(); + } + if (EncodingType.RICH_CURSOR == rect.getEncodingType() || + EncodingType.CURSOR_POS == rect.getEncodingType()) { + repaintController.repaintCursor(); + } else if (rect.getEncodingType() == EncodingType.DESKTOP_SIZE) { + synchronized (this) { + fullscreenFbUpdateIncrementalRequest = + new FramebufferUpdateRequestMessage(0, 0, rect.width, rect.height, true); + } + renderer = repaintController.createRenderer(transport, rect.width, rect.height, + protocol.getPixelFormat()); + protocol.sendMessage(new FramebufferUpdateRequestMessage(0, 0, rect.width, rect.height, false)); + return; + } else { + repaintController.repaintBitmap(rect); + } + } else { + throw new CommonException("Unprocessed encoding: " + rect.toString()); + } + } + if (needSendPixelFormat) { + synchronized (this) { + if (needSendPixelFormat) { + needSendPixelFormat = false; + protocol.setPixelFormat(pixelFormat); + protocol.sendMessage(new SetPixelFormatMessage(pixelFormat)); + logger.fine("sent: " + pixelFormat); + protocol.sendRefreshMessage(); + logger.fine("sent: nonincremental fb update"); + } + } + } else { + protocol.sendMessage(fullscreenFbUpdateIncrementalRequest); + } + } + + public synchronized void queueUpdatePixelFormat(PixelFormat pf) { + pixelFormat = pf; + needSendPixelFormat = true; +// protocol.sendMessage(new FramebufferUpdateRequestMessage(0, 0, 1, 1, false)); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/SenderTask.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/SenderTask.java new file mode 100644 index 0000000..2f069a2 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/SenderTask.java @@ -0,0 +1,79 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.client.ClientToServerMessage; +import com.glavsoft.transport.Transport; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.Logger; + +public class SenderTask implements Runnable { + + private final MessageQueue queue; + private final Transport transport; + private final Protocol protocol; + + /** + * Create sender task + * Task runs as thread, receive messages from queue and sends them to transport. + * When no messages appears in queue longer than timeout period, sends FramebufferUpdate + * request + * @param messageQueue queue to poll messages + * @param transport transport to send messages out + * @param protocol session lifecircle support + */ + public SenderTask(MessageQueue messageQueue, Transport transport, Protocol protocol) { + this.queue = messageQueue; + this.transport = transport; + this.protocol = protocol; + } + + @Override + public void run() { + ClientToServerMessage message; + try { + while ( ! Thread.currentThread().isInterrupted()) { + message = queue.get(); + if (message != null) { + message.send(transport); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TransportException e) { + Logger.getLogger(getClass().getName()).severe("Close session: " + e.getMessage()); + protocol.cleanUpSession("Connection closed"); + } catch (Throwable te) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + te.printStackTrace(pw); + protocol.cleanUpSession(te.getMessage() + "\n" + sw.toString()); + } + Logger.getLogger(getClass().getName()).finer("Sender task stopped"); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/AuthHandler.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/AuthHandler.java new file mode 100644 index 0000000..33e6d80 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/AuthHandler.java @@ -0,0 +1,117 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.auth; + +import com.glavsoft.exceptions.AuthenticationFailedException; +import com.glavsoft.exceptions.ClosedConnectionException; +import com.glavsoft.exceptions.FatalException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.exceptions.UnsupportedSecurityTypeException; +import com.glavsoft.rfb.encoding.ServerInitMessage; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.transport.Transport; + +import java.util.logging.Logger; + +public abstract class AuthHandler { + private static final int AUTH_RESULT_OK = 0; +// private static final int AUTH_RESULT_FAILED = 1; + private Logger logger; + + /** + * Not thread safe, no need to be thread safe + */ + protected Logger logger() { + if (null == logger) { + logger = Logger.getLogger(getClass().getName()); + } + return logger; + } + /** + * Authenticate using appropriate auth scheme + * + * @param transport transport for i/o + * @param protocol rfb protocol object + * @return transport for future i/o using + */ + public abstract Transport authenticate(Transport transport, Protocol protocol) + throws TransportException, FatalException, UnsupportedSecurityTypeException; + public abstract SecurityType getType(); + public int getId() { + return getType().getId(); + } + public String getName() { + return getType().name(); + } + + /** + * Check Security Result received from server + * May be: + * * 0 - OK + * * 1 - Failed + * + * Do not check on NoneAuthentication + */ + public void checkSecurityResult(Transport transport) throws TransportException, + AuthenticationFailedException { + final int securityResult = transport.readInt32(); + logger().fine("Security result: " + securityResult + (AUTH_RESULT_OK == securityResult ? " (OK)" : " (Failed)")); + if (securityResult != AUTH_RESULT_OK) { + try { + String reason = transport.readString(); + logger().fine("Security result reason: " + reason); + throw new AuthenticationFailedException(reason); + } catch (ClosedConnectionException e) { + // protocol version 3.3 and 3.7 does not send reason string, + // but silently closes the connection + throw new AuthenticationFailedException("Authentication failed"); + } + } + } + + public void initProcedure(Transport transport, Protocol protocol) throws TransportException { + sendClientInitMessage(transport, protocol.getSettings().getSharedFlag()); + ServerInitMessage serverInitMessage = readServerInitMessage(transport); + completeContextData(serverInitMessage, protocol); + protocol.registerRfbEncodings(); + } + + protected ServerInitMessage readServerInitMessage(Transport transport) throws TransportException { + final ServerInitMessage serverInitMessage = new ServerInitMessage().readFrom(transport); + logger().fine("Read: " + serverInitMessage); + return serverInitMessage; + } + + protected void sendClientInitMessage(Transport transport, byte sharedFlag) throws TransportException { + logger().fine("Sent client-init-message: " + sharedFlag); + transport.writeByte(sharedFlag).flush(); + } + + protected void completeContextData(ServerInitMessage serverInitMessage, Protocol protocol) { + protocol.setServerPixelFormat(serverInitMessage.getPixelFormat()); + protocol.setFbWidth(serverInitMessage.getFramebufferWidth()); + protocol.setFbHeight(serverInitMessage.getFramebufferHeight()); + protocol.setRemoteDesktopName(serverInitMessage.getName()); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/NoneAuthentication.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/NoneAuthentication.java new file mode 100644 index 0000000..4dc24b0 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/NoneAuthentication.java @@ -0,0 +1,42 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.auth; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.transport.Transport; + +public class NoneAuthentication extends AuthHandler { + + @Override + public Transport authenticate(Transport transport, Protocol protocol) throws TransportException { + return transport; + } + + @Override + public SecurityType getType() { + return SecurityType.NONE_AUTHENTICATION; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/SecurityType.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/SecurityType.java new file mode 100644 index 0000000..14fd4fd --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/SecurityType.java @@ -0,0 +1,49 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.auth; + +/** + * Security types that implemented + */ +public enum SecurityType { + NONE_AUTHENTICATION(1), + VNC_AUTHENTICATION(2), +// int RA2_AUTHENTICATION = 5; +// int RA2NE_AUTHENTICATION = 6; + TIGHT_AUTHENTICATION(16), + TIGHT2_AUTHENTICATION(116); +// int ULTRA_AUTHENTICATION = 17; +// int TLS_AUTHENTICATION = 18; +// int VENCRYPT_AUTHENTICATION = 19; + + private int id; + private SecurityType(int id) { + this.id = id; + } + + public int getId() { + return id; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/TightAuthentication.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/TightAuthentication.java new file mode 100644 index 0000000..54a549b --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/TightAuthentication.java @@ -0,0 +1,268 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.auth; + +import com.glavsoft.exceptions.FatalException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.exceptions.UnsupportedSecurityTypeException; +import com.glavsoft.rfb.RfbCapabilityInfo; +import com.glavsoft.rfb.encoding.ServerInitMessage; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.tunnel.TunnelHandler; +import com.glavsoft.rfb.protocol.tunnel.TunnelType; +import com.glavsoft.transport.Transport; +import com.glavsoft.utils.Strings; + +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class TightAuthentication extends AuthHandler { + private final Map registeredAuthHandlers = new HashMap(); + private final Map registeredTunnelHandlers = new HashMap(); + + public TightAuthentication() { + } + + public void registerTunnelingHandler(TunnelHandler handler) { + registeredTunnelHandlers.put(handler.getId(), handler); + } + + public void registerAuthHandler(AuthHandler handler) { + registeredAuthHandlers.put(handler.getId(), handler); + } + + @Override + public SecurityType getType() { + return SecurityType.TIGHT_AUTHENTICATION; + } + + @Override + public Transport authenticate(Transport transport, Protocol protocol) + throws TransportException, FatalException, UnsupportedSecurityTypeException { + transport = tunnelingNegotiation(transport, protocol); + authorizationNegotiation(transport, protocol); + protocol.setTight(true); + return transport; + } + + @Override + public void initProcedure(Transport transport, Protocol protocol) throws TransportException { + capabilitiesNegotiation(transport, protocol); + protocol.registerRfbEncodings(); + } + + /** + * Capabilities negotiation consists of server-to-client message, where server introduces its capabilities, + * and client-to-server message, which introduces only those client capabilities which are supported by server + * and encodings supported by server. + * + * This data immediately follows the server initialisation message. + * + * typedef struct _rfbInteractionCapsMsg { + * CARD16 nServerMessageTypes; + * CARD16 nClientMessageTypes; + * CARD16 nEncodingTypes; + * CARD16 pad;><------><------>// reserved, must be 0 + * // followed by nServerMessageTypes * rfbCapabilityInfo structures + * // followed by nClientMessageTypes * rfbCapabilityInfo structures + * } rfbInteractionCapsMsg; + * #define sz_rfbInteractionCapsMsg 8 + * + * nServerMessageTypes | UINT16 | Number of server message types server announces. + * nClientMessageTypes | UINT16 | Number of client message types server announces. + * nEncodingTypes | UINT16 | Number of encoding types server announces. + * ServerMessageTypes | RFBCAPABILITY x nServerMessageTypes | Server side messages which server supports. + * ClientMessageTypes | RFBCAPABILITY x nClientMessageTypes | Client side messages which server supports. + * Encodings | RFBCAPABILITY x nEncodingTypes | Encoding types which server supports. + * + * Client replies with message in exactly the same format, listing only those capabilities which are supported + * both by server and the client. + * Once all three initialization stages are successfully finished, client and server switch to normal protocol flow. + * + */ + void capabilitiesNegotiation(Transport transport, Protocol protocol) throws TransportException { + sendClientInitMessage(transport, protocol.getSettings().getSharedFlag()); + ServerInitMessage serverInitMessage = readServerInitMessage(transport); + + int nServerMessageTypes = transport.readUInt16(); + int nClientMessageTypes = transport.readUInt16(); + int nEncodingTypes = transport.readUInt16(); + transport.readUInt16(); //padding + + logger().fine("nServerMessageTypes: " + nServerMessageTypes + ", nClientMessageTypes: " + nClientMessageTypes + + ", nEncodingTypes: " + nEncodingTypes); + + registerServerMessagesTypes(transport, protocol, nServerMessageTypes); + registerClientMessagesTypes(transport, protocol, nClientMessageTypes); + registerEncodings(transport, protocol, nEncodingTypes); + completeContextData(serverInitMessage, protocol); + } + + private void registerServerMessagesTypes(Transport transport, Protocol protocol, int count) throws TransportException { + while (count-- > 0) { + RfbCapabilityInfo capInfoReceived = new RfbCapabilityInfo().readFrom(transport); + logger().fine("Server message type: " + capInfoReceived.toString()); + } + } + + private void registerClientMessagesTypes(Transport transport, Protocol protocol, int count) throws TransportException { + while (count-- > 0) { + RfbCapabilityInfo capInfoReceived = new RfbCapabilityInfo().readFrom(transport); + logger().fine("Client message type: " + capInfoReceived.toString()); + protocol.registerClientMessageType(capInfoReceived); + } + } + + private void registerEncodings(Transport transport, Protocol protocol, int count) throws TransportException { + while (count-- > 0) { + RfbCapabilityInfo capInfoReceived = new RfbCapabilityInfo().readFrom(transport); + logger().fine("Encoding: " + capInfoReceived.toString()); + protocol.registerEncoding(capInfoReceived); + } + } + + /** + * Negotiation of Tunneling Capabilities (protocol versions 3.7t, 3.8t) + * + * If the chosen security type is rfbSecTypeTight, the server sends a list of + * supported tunneling methods ("tunneling" refers to any additional layer of + * data transformation, such as encryption or external compression.) + * + * nTunnelTypes specifies the number of following rfbCapabilityInfo structures + * that list all supported tunneling methods in the order of preference. + * + * NOTE: If nTunnelTypes is 0, that tells the client that no tunneling can be + * used, and the client should not send a response requesting a tunneling + * method. + * + * typedef struct _rfbTunnelingCapsMsg { + * CARD32 nTunnelTypes; + * //followed by nTunnelTypes * rfbCapabilityInfo structures + * } rfbTunnelingCapsMsg; + * #define sz_rfbTunnelingCapsMsg 4 + * ---------------------------------------------------------------------------- + * Tunneling Method Request (protocol versions 3.7t, 3.8t) + * + * If the list of tunneling capabilities sent by the server was not empty, the + * client should reply with a 32-bit code specifying a particular tunneling + * method. The following code should be used for no tunneling. + * + * #define rfbNoTunneling 0 + * #define sig_rfbNoTunneling "NOTUNNEL" + */ + Transport tunnelingNegotiation(Transport transport, Protocol protocol) + throws TransportException { + Transport newTransport = transport; + int tunnelsCount; + tunnelsCount = (int) transport.readUInt32(); + logger().fine("Tunneling capabilities: " + tunnelsCount); + int [] tunnelCodes = new int[tunnelsCount]; + if (tunnelsCount > 0) { + for (int i = 0; i < tunnelsCount; ++i) { + RfbCapabilityInfo rfbCapabilityInfo = new RfbCapabilityInfo().readFrom(transport); + tunnelCodes[i] = rfbCapabilityInfo.getCode(); + logger().fine(rfbCapabilityInfo.toString()); + } + int selectedTunnelCode; + if (tunnelsCount > 0) { + for (int i = 0; i < tunnelsCount; ++i) { + final TunnelHandler tunnelHandler = registeredTunnelHandlers.get(tunnelCodes[i]); + if (tunnelHandler != null) { + selectedTunnelCode = tunnelCodes[i]; + transport.writeInt32(selectedTunnelCode).flush(); + logger().fine("Accepted tunneling type: " + selectedTunnelCode); + newTransport = tunnelHandler.createTunnel(transport); + logger().fine("Tunnel created: " + TunnelType.byCode(selectedTunnelCode)); + protocol.setTunnelType(TunnelType.byCode(selectedTunnelCode)); + break; + } + } + } + } + if (protocol.getTunnelType() == null) { + protocol.setTunnelType(TunnelType.NOTUNNEL); + if (tunnelsCount > 0) { + transport.writeInt32(TunnelType.NOTUNNEL.code).flush(); + } + logger().fine("Accepted tunneling type: " + TunnelType.NOTUNNEL); + } + return newTransport; + } + + /** + * Negotiation of Authentication Capabilities (protocol versions 3.7t, 3.8t) + * + * After setting up tunneling, the server sends a list of supported + * authentication schemes. + * + * nAuthTypes specifies the number of following rfbCapabilityInfo structures + * that list all supported authentication schemes in the order of preference. + * + * NOTE: If nAuthTypes is 0, that tells the client that no authentication is + * necessary, and the client should not send a response requesting an + * authentication scheme. + * + * typedef struct _rfbAuthenticationCapsMsg { + * CARD32 nAuthTypes; + * // followed by nAuthTypes * rfbCapabilityInfo structures + * } rfbAuthenticationCapsMsg; + * #define sz_rfbAuthenticationCapsMsg 4 + * + */ + void authorizationNegotiation(Transport transport, Protocol protocol) + throws UnsupportedSecurityTypeException, TransportException, FatalException { + int authCount; + authCount = transport.readInt32(); + logger().fine("Auth capabilities: " + authCount); + byte[] cap = new byte[authCount]; + for (int i = 0; i < authCount; ++i) { + RfbCapabilityInfo rfbCapabilityInfo = new RfbCapabilityInfo().readFrom(transport); + cap[i] = (byte) rfbCapabilityInfo.getCode(); + logger().fine(rfbCapabilityInfo.toString()); + } + AuthHandler authHandler = null; + if (authCount > 0) { + for (int i = 0; i < authCount; ++i) { + authHandler = registeredAuthHandlers.get((int) cap[i]); + if (authHandler != null) { + //sending back RFB capability code + transport.writeInt32(authHandler.getId()).flush(); + break; + } + } + } else { + authHandler = registeredAuthHandlers.get(SecurityType.NONE_AUTHENTICATION.getId()); + } + if (null == authHandler) { + throw new UnsupportedSecurityTypeException("Server auth types: " + Strings.toString(cap) + + ", supported auth types: " + registeredAuthHandlers.values()); + } + logger().fine("Auth capability accepted: " + authHandler.getName()); + authHandler.authenticate(transport, protocol); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/VncAuthentication.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/VncAuthentication.java new file mode 100644 index 0000000..c5c7219 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/auth/VncAuthentication.java @@ -0,0 +1,103 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.auth; + +import com.glavsoft.exceptions.CryptoException; +import com.glavsoft.exceptions.FatalException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.transport.Transport; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import static com.glavsoft.utils.Strings.getBytesWithCharset; + +public class VncAuthentication extends AuthHandler { + @Override + public SecurityType getType() { + return SecurityType.VNC_AUTHENTICATION; + } + + @Override + public Transport authenticate(Transport transport, Protocol protocol) + throws TransportException, FatalException { + byte [] challenge = transport.readBytes(16); + String password = protocol.getPasswordRetriever().getResult(); + if (null == password) password = ""; + byte [] key = new byte[8]; + System.arraycopy(getBytesWithCharset(password, Transport.ISO_8859_1), 0, key, 0, Math.min(key.length, getBytesWithCharset(password, Transport.ISO_8859_1).length)); + transport.write(encrypt(challenge, key)).flush(); + return transport; + } + + /** + * Encrypt challenge by key using DES + * @return encrypted bytes + * @throws CryptoException on problem with DES algorithm support or smth about + */ + public byte[] encrypt(byte[] challenge, byte[] key) throws CryptoException { + try { + DESKeySpec desKeySpec = new DESKeySpec(mirrorBits(key)); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + Cipher desCipher = Cipher.getInstance("DES/ECB/NoPadding"); + desCipher.init(Cipher.ENCRYPT_MODE, secretKey); + return desCipher.doFinal(challenge); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException("Cannot encrypt challenge", e); + } catch (NoSuchPaddingException e) { + throw new CryptoException("Cannot encrypt challenge", e); + } catch (IllegalBlockSizeException e) { + throw new CryptoException("Cannot encrypt challenge", e); + } catch (BadPaddingException e) { + throw new CryptoException("Cannot encrypt challenge", e); + } catch (InvalidKeyException e) { + throw new CryptoException("Cannot encrypt challenge", e); + } catch (InvalidKeySpecException e) { + throw new CryptoException("Cannot encrypt challenge", e); + } + } + + private byte[] mirrorBits(byte[] k) { + byte[] key = new byte[8]; + for (int i = 0; i < 8; i++) { + byte s = k[i]; + s = (byte) (((s >> 1) & 0x55) | ((s << 1) & 0xaa)); + s = (byte) (((s >> 2) & 0x33) | ((s << 2) & 0xcc)); + s = (byte) (((s >> 4) & 0x0f) | ((s << 4) & 0xf0)); + key[i] = s; + } + return key; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/handlers/Handshaker.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/handlers/Handshaker.java new file mode 100644 index 0000000..6d2d8f4 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/handlers/Handshaker.java @@ -0,0 +1,345 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.handlers; + +import com.glavsoft.exceptions.AuthenticationFailedException; +import com.glavsoft.exceptions.FatalException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.exceptions.UnsupportedProtocolVersionException; +import com.glavsoft.exceptions.UnsupportedSecurityTypeException; +import com.glavsoft.rfb.IRequestString; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.auth.AuthHandler; +import com.glavsoft.rfb.protocol.auth.NoneAuthentication; +import com.glavsoft.rfb.protocol.auth.SecurityType; +import com.glavsoft.rfb.protocol.auth.TightAuthentication; +import com.glavsoft.rfb.protocol.auth.VncAuthentication; +import com.glavsoft.rfb.protocol.tunnel.SslTunnel; +import com.glavsoft.rfb.protocol.tunnel.TunnelType; +import com.glavsoft.transport.Transport; +import com.glavsoft.utils.Strings; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author dime at glavsoft.com + */ +public class Handshaker { + private static final int PROTOCOL_STRING_LENGTH = 12; + private static final String RFB_PROTOCOL_STRING_REGEXP = "^RFB (\\d\\d\\d).(\\d\\d\\d)\n$"; + private static final String DISPATCHER_PROTOCOL_STRING = "TCPDISPATCH\n"; + + private static final int MIN_SUPPORTED_VERSION_MAJOR = 3; + private static final int MIN_SUPPORTED_VERSION_MINOR = 3; + + private static final int MAX_SUPPORTED_VERSION_MAJOR = 3; + private static final int MAX_SUPPORTED_VERSION_MINOR = 8; + protected static final int DISPATCHER_PROTOCOL_VERSION = 3; + protected static final int KEEP_ALIVE_BYTE = 0; + protected static final int START_BYTE = 1; + private Protocol protocol; + private Logger logger; + private final Map registeredAuthHandlers = new HashMap(); + + public Handshaker(Protocol protocol) { + this.protocol = protocol; + logger = Logger.getLogger(getClass().getName()); + registerAuthHandler(SecurityType.NONE_AUTHENTICATION.getId(), new NoneAuthentication()); + registerAuthHandler(SecurityType.VNC_AUTHENTICATION.getId(), new VncAuthentication()); + + final TightAuthentication tightAuthentication = new TightAuthentication(); + tightAuthentication.registerAuthHandler(new NoneAuthentication()); + tightAuthentication.registerAuthHandler(new VncAuthentication()); + if (protocol.getSettings().getTunnelType() != TunnelType.NOTUNNEL && + SslTunnel.isTransportAvailable()) { + tightAuthentication.registerTunnelingHandler(new SslTunnel()); + registerAuthHandler(SecurityType.TIGHT2_AUTHENTICATION.getId(), tightAuthentication); + } + registerAuthHandler(SecurityType.TIGHT_AUTHENTICATION.getId(), tightAuthentication); + } + + public Transport handshake(Transport transport) throws TransportException, UnsupportedProtocolVersionException, AuthenticationFailedException, FatalException, UnsupportedSecurityTypeException { + String protocolString = transport.readString(PROTOCOL_STRING_LENGTH); + if (isDispatcherConnection(protocolString)) { + handshakeToDispatcher(transport); + protocolString = transport.readString(PROTOCOL_STRING_LENGTH); + } + ProtocolVersion ver = matchProtocolVersion(protocolString); + transport.write(Strings.getBytesWithCharset("RFB 00" + ver.major + ".00" + ver.minor + "\n", Transport.ISO_8859_1)).flush(); + protocol.setProtocolVersion(ver); + logger.info("Set protocol version to: " + ver); + transport = auth(transport, ver); + return transport; + } + + /** + * Make dispatcher connection + * + * Dispatcher protocol v.3: '<-' means receive from dispatcher, '->' means send to dispatcher + * <- "TCPDISPATCH\n" — already received at this point + * <- UInt8 numSupportedVersions value + * <- numSupportedVersions UInt8 values of supported version num + * -> UInt8 value of version accepted + * -> UInt8 remoteHostRole value (0 == RFB Server or 1 == RFB Client/viewer) + * -> UInt32 connId + * <- UInt32 connId (when 0 == connId, then dispatcher generates unique random connId value + * and sends it to clients, else it doesn't send the one) + * -> UInt8 secret keyword string length + * -> String (byte array of ASCII characters) - secret keyword + * -> UInt8 dispatcher name string length (may equals to 0) + * -> String (byte array of ASCII characters) - dispatcher name + * <- UInt8 dispatcher name string length + * <- String (byte array of ASCII characters) - dispatcher name + * <- 0 'keep alive byte' or non zero 'start byte' (1) + * On keep alive byte immediately answer with the same byte, and wain for next byte, + * on start byte go to ordinary rfb negotiation. + * + * @param transport + * + * @throws TransportException when some io error happens + * @throws UnsupportedProtocolVersionException when protocol doesn't match + * @throws AuthenticationFailedException when connectionId provided by user is wrong + */ + private void handshakeToDispatcher(Transport transport) throws TransportException, UnsupportedProtocolVersionException, AuthenticationFailedException { + int numSupportedVersions = transport.readUInt8(); // receive num of supported version followed (u8) + List remoteVersions = new ArrayList(numSupportedVersions); + for (int i = 0; i < numSupportedVersions; ++i) { + remoteVersions.add(transport.readUInt8()); // receive supported protocol versions (numSupportedVersions x u8) + } + logger.fine("Dispatcher protocol versions: " + Arrays.toString(remoteVersions.toArray())); + if (!remoteVersions.contains(DISPATCHER_PROTOCOL_VERSION)) { + throw new UnsupportedProtocolVersionException("Dispatcher unsupported protocol versions"); + } + transport.writeByte(DISPATCHER_PROTOCOL_VERSION); // send protocol version we use (u8) + transport.writeByte(1).flush(); // send we are the viewer (u8) + long connectionId = 0; + IRequestString connIdRetriever = protocol.getConnectionIdRetriever(); + if (null == connIdRetriever) throw new IllegalStateException("ConnectionIdRetriever is null"); + String sId = connIdRetriever.getResult(); + if (Strings.isTrimmedEmpty(sId)) throw new AuthenticationFailedException("ConnectionId is empty"); + try { + connectionId = Long.parseLong(sId); + } catch (NumberFormatException nfe) { + throw new AuthenticationFailedException("Wrong ConnectionId"); + } + if ( 0 == connectionId) { + throw new AuthenticationFailedException("ConnectionId have not be equals to zero"); + } + transport.writeUInt32(connectionId).flush(); // send connectionId (u32) + + transport.writeByte(0); // send UInt8 secret keyword string length. 0 - for none + // send String (byte array of ASCII characters) - secret keyword. + // Skip if none + transport.writeByte(0).flush(); // send UInt8 dispatcher name string length (may equals to 0) + // send -> String (byte array of ASCII characters) - dispatcher name. + // Skip if none + //logger.fine("Sent: version3, viewer, connectionId: " + connectionId + " secret:0, token: 0"); + int tokenLength = transport.readUInt8(); // receive UInt8 token length + // receive byte array - dispatcher token + byte [] token = transport.readBytes(tokenLength); + //logger.fine("token: #" + tokenLength + " " + (tokenLength>0?token[0]:"") +(tokenLength>1?token[1]:"")+(tokenLength>2?token[2]:"")); + // receive 0 'keep alive byte' or non zero 'start byte' (1) + // on keep alive byte send the same to remote + // on start byte go to starting rfb connection + int b; + do { + b = transport.readByte(); + if (KEEP_ALIVE_BYTE == b) { + logger.finer("keep-alive"); + transport.writeByte(KEEP_ALIVE_BYTE).flush(); + } + } while (b != START_BYTE); + logger.info("Dispatcher handshake completed"); + } + + /** + * When first 12 bytes sent by server is "TCPDISPATCH\n" this is dispatcher connection + * + * @param protocolString string with first 12 bytes sent by server + * @return true when we connects to dispatcher, not remote rfb server + */ + private boolean isDispatcherConnection(String protocolString) { + final boolean dispatcherDetected = DISPATCHER_PROTOCOL_STRING.equals(protocolString); + if (dispatcherDetected) { + logger.info("Dispatcher connection detected"); + } + return dispatcherDetected; + } + + /** + * Take first 12 bytes sent by server and match rfb protocol version. + * RFB protocol version string is "RFB MMM.mmm\n". Where MMM is major + * protocol version and mmm is minor one. + * + * Side effect: set protocol.isMac when MacOs at other side is detected + * + * @param protocolString string with first 12 bytes sent by server + * @return version of protocol will be used + */ + private ProtocolVersion matchProtocolVersion(String protocolString) throws UnsupportedProtocolVersionException { + logger.info("Server protocol string: " + protocolString.substring(0, protocolString.length() - 1)); + Pattern pattern = Pattern.compile(RFB_PROTOCOL_STRING_REGEXP); + final Matcher matcher = pattern.matcher(protocolString); + if ( ! matcher.matches()) + throw new UnsupportedProtocolVersionException( + "Unsupported protocol version: " + protocolString); + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + ProtocolVersion ver; + boolean isMac = false; + if (889 == minor) { + isMac = true; + } + if (major < MIN_SUPPORTED_VERSION_MAJOR || + MIN_SUPPORTED_VERSION_MAJOR == major && minor < MIN_SUPPORTED_VERSION_MINOR) + throw new UnsupportedProtocolVersionException( + "Unsupported protocol version: " + major + "." + minor); + if (major > MAX_SUPPORTED_VERSION_MAJOR) { +// major = MAX_SUPPORTED_VERSION_MAJOR; + minor = MAX_SUPPORTED_VERSION_MINOR; + } + + if (minor >= MIN_SUPPORTED_VERSION_MINOR && minor < 7) { + ver = ProtocolVersion.PROTOCOL_VERSION_3_3; + } else if (7 == minor) { + ver = ProtocolVersion.PROTOCOL_VERSION_3_7; + } else if (minor >= MAX_SUPPORTED_VERSION_MINOR) { + ver = ProtocolVersion.PROTOCOL_VERSION_3_8; + } else + throw new UnsupportedProtocolVersionException("Unsupported protocol version: " + protocolString); + protocol.setMac(isMac); + return ver; + } + + private Transport auth(Transport transport, ProtocolVersion ver) throws UnsupportedSecurityTypeException, TransportException, FatalException, AuthenticationFailedException { + AuthHandler handler; + switch (ver) { + case PROTOCOL_VERSION_3_3: + handler = auth33(transport); + break; + case PROTOCOL_VERSION_3_7: + handler = auth37_38(transport); + break; + case PROTOCOL_VERSION_3_8: + handler = auth37_38(transport); + break; + default: + throw new IllegalStateException(); + } + transport = handler.authenticate(transport, protocol); + if (ver == ProtocolVersion.PROTOCOL_VERSION_3_8 || + handler.getType() != SecurityType.NONE_AUTHENTICATION) { + handler.checkSecurityResult(transport); + } + handler.initProcedure(transport, protocol); + return transport; + } + + private AuthHandler auth33(Transport transport) throws TransportException, UnsupportedSecurityTypeException { + int type = transport.readInt32(); + logger.info("Type received: " + type); + if (0 == type) + throw new UnsupportedSecurityTypeException(transport.readString()); + AuthHandler handler = registeredAuthHandlers.get(selectAuthHandlerId((byte) (0xff & type))); + return handler; + } + + private AuthHandler auth37_38(Transport transport) throws TransportException, UnsupportedSecurityTypeException { + int secTypesNum = transport.readUInt8(); + if (0 == secTypesNum) + throw new UnsupportedSecurityTypeException(transport.readString()); + byte[] secTypes = transport.readBytes(secTypesNum); + logger.info("Security Types received (" + secTypesNum + "): " + Strings.toString(secTypes)); + final int typeIdAccepted = selectAuthHandlerId(secTypes); + final AuthHandler authHandler = registeredAuthHandlers.get(typeIdAccepted); + transport.writeByte(typeIdAccepted).flush(); + return authHandler; + } + + private int selectAuthHandlerId(byte... secTypes) + throws UnsupportedSecurityTypeException, TransportException { + AuthHandler handler; + // Tight2 Authentication very first + for (byte type : secTypes) { + if (SecurityType.TIGHT2_AUTHENTICATION.getId() == (0xff & type)) { + handler = registeredAuthHandlers.get(SecurityType.TIGHT2_AUTHENTICATION.getId()); + if (handler != null) { + logger.info("Security Type accepted: " + SecurityType.TIGHT2_AUTHENTICATION.name()); + return SecurityType.TIGHT2_AUTHENTICATION.getId(); + } + } + } + // Tight Authentication first + for (byte type : secTypes) { + if (SecurityType.TIGHT_AUTHENTICATION.getId() == (0xff & type)) { + handler = registeredAuthHandlers.get(SecurityType.TIGHT_AUTHENTICATION.getId()); + if (handler != null) { + logger.info("Security Type accepted: " + SecurityType.TIGHT_AUTHENTICATION.name()); + return SecurityType.TIGHT_AUTHENTICATION.getId(); + } + } + } + for (byte type : secTypes) { + handler = registeredAuthHandlers.get(0xff & type); + if (handler != null) { + logger.info("Security Type accepted: " + handler.getType()); + return handler.getType().getId(); + } + } + throw new UnsupportedSecurityTypeException( + "No security types supported. Server sent '" + + Strings.toString(secTypes) + + "' security types, but we do not support any of their."); + } + + private void registerAuthHandler(int id, AuthHandler handler) { + registeredAuthHandlers.put(id, handler); + } + + public static enum ProtocolVersion { + PROTOCOL_VERSION_3_3(3, 3), + PROTOCOL_VERSION_3_7(3, 7), + PROTOCOL_VERSION_3_8(3, 8); + + public final int minor; + public final int major; + + ProtocolVersion(int major, int minor) { + this.major = major; + this.minor = minor; + } + + @Override + public String toString() { + return String.valueOf(major) + "." + String.valueOf(minor); + } + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/SslTunnel.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/SslTunnel.java new file mode 100644 index 0000000..73c66c7 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/SslTunnel.java @@ -0,0 +1,121 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.tunnel; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +/** + * @author dime at glavsoft.com + */ +public class SslTunnel implements TunnelHandler { + + /** + * Requested protocol (algorithm) name. + * May be one of the follows: + * SSL Supports some version of SSL; may support other versions + * SSLv2 Supports SSL version 2 or later; may support other versions + * SSLv3 Supports SSL version 3; may support other versions + * TLS Supports some version of TLS; may support other versions + * TLSv1 Supports RFC 2246: TLS version 1.0 ; may support other versions + * TLSv1.1 Supports RFC 4346: TLS version 1.1 ; may support other versions + * TLSv1.2 Supports RFC 5246: TLS version 1.2 ; may support other versions + * + * Using TLSv1.1 because earlier versions have vulnerabilities. + */ + private static final String PROTOCOL = "TLSv1.1"; + private static final String SSL_TRANSPORT = "com.glavsoft.transport.SslTransport"; + + public SslTunnel() { + } + + @Override + public int getId() { + return TunnelType.SSL.code; + } + + @Override + public Transport createTunnel(Transport transport) throws TransportException { + try { + SSLContext sslContext = SSLContext.getInstance(PROTOCOL); + sslContext.init(null, getTrustAllCertsManager(), null); + SSLEngine engine = sslContext.createSSLEngine(); + engine.setUseClientMode(true); + @SuppressWarnings("unchecked") + final Class sslTransportClass = (Class) Class.forName(SSL_TRANSPORT); + final Constructor constructor = sslTransportClass.getConstructor(Transport.class, SSLEngine.class); + return constructor.newInstance(transport, engine); + } catch (NoSuchAlgorithmException e) { + throw new TransportException("Cannot create SSL/TLS tunnel", e); + } catch (KeyManagementException e) { + throw new TransportException("Cannot create SSL/TLS tunnel", e); + } catch (ClassNotFoundException e) { + throw new TransportException("Cannot create SSL/TLS tunnel, SSL transport plugin unavailable", e); + } catch (NoSuchMethodException e) { + throw new TransportException("Cannot create SSL/TLS tunnel, SSL transport plugin unavailable", e); + } catch (InvocationTargetException e) { + throw new TransportException("Cannot create SSL/TLS tunnel, SSL transport plugin unavailable", e); + } catch (InstantiationException e) { + throw new TransportException("Cannot create SSL/TLS tunnel, SSL transport plugin unavailable", e); + } catch (IllegalAccessException e) { + throw new TransportException("Cannot create SSL/TLS tunnel, SSL transport plugin unavailable", e); + } + + } + + private TrustManager[] getTrustAllCertsManager() { + return new TrustManager[]{ + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + } + }; + } + + + public static boolean isTransportAvailable() { + try { + Class.forName("com.glavsoft.transport.SslTransport"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelHandler.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelHandler.java new file mode 100644 index 0000000..801d40d --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelHandler.java @@ -0,0 +1,36 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.tunnel; + +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.transport.Transport; + +/** + * @author dime at glavsoft.com + */ +public interface TunnelHandler { + public int getId(); + + Transport createTunnel(Transport transport) throws TransportException; +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelType.java b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelType.java new file mode 100644 index 0000000..0c2e407 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/rfb/protocol/tunnel/TunnelType.java @@ -0,0 +1,55 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.rfb.protocol.tunnel; + +import com.glavsoft.rfb.RfbCapabilityInfo; + +/** + * @author dime at tightvnc.com + */ +public enum TunnelType { + NOTUNNEL(0, RfbCapabilityInfo.VENDOR_STANDARD, "NOTUNNEL", ""), + + SSL(2, RfbCapabilityInfo.VENDOR_TIGHT, "SSL_____", "SSL/TLS"); + + public final int code; + public final String vendor; + public final String name; + public final String hrName; + + TunnelType(int code, String vendor, String name, String humanReadableName) { + this.code = code; + this.vendor = vendor; + this.name = name; + hrName = humanReadableName; + } + + public static TunnelType byCode(int code) { + for (TunnelType type : values()) { + if (type.code == code) return type; + } + return null; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/transport/BaudrateMeter.java b/plugins/vnc/src/main/java/com/glavsoft/transport/BaudrateMeter.java new file mode 100644 index 0000000..2433f7a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/transport/BaudrateMeter.java @@ -0,0 +1,65 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.transport; + +/** + * @author dime at tightvnc.com + */ +public class BaudrateMeter { + + public static final int MIN_BPS = 10000; + private static final int n = 5; + private static final double ALPHA = 2. / (n + 1); + private double ema = 0; + private boolean measure = false; + private long start; + private long bytes; + + public void count(int bytes) { + if (measure) this.bytes += bytes; + } + + public int kBPS() { + return (int) (ema / 1000); + } + + public void startMeasuringCycle() { + measure = true; + start = System.currentTimeMillis(); + } + + public void stopMeasuringCycle() { + measure = false; + long ms = System.currentTimeMillis() - start; + if (0 == ms || bytes < 100) return; // skip with too small portion of data + double bps = bytes * 8. / (ms / 1000.); +// double bpms = bytes * 8. / ms; + if (bps < MIN_BPS) { // limit lower value + bps = MIN_BPS; + } + // exponential moving-average smoothing + ema = ALPHA * bps + (1. - ALPHA) * (0. == ema ? bps : ema); + bytes = 0; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/transport/Transport.java b/plugins/vnc/src/main/java/com/glavsoft/transport/Transport.java new file mode 100644 index 0000000..4d85be4 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/transport/Transport.java @@ -0,0 +1,349 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.transport; + +import com.glavsoft.exceptions.ClosedConnectionException; +import com.glavsoft.exceptions.TransportException; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * @author dime + */ +public class Transport { + public final static Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; + public final static Charset UTF8 = StandardCharsets.UTF_8; + DataInputStream is; + DataOutputStream os; + InputStream origIs; + OutputStream origOs; + private BaudrateMeter baudrateMeter; + + public Transport(Socket socket) throws IOException { + this(new BufferedInputStream(socket.getInputStream()), new BufferedOutputStream(socket.getOutputStream())); + } + + public Transport(InputStream is) { + this(is, null); + } + + public Transport(OutputStream os) { + this(null, os); + } + + public Transport(InputStream is, OutputStream os) { + init(is, os); + } + + void init(InputStream is, OutputStream os) { + origIs = is; + this.is = is != null ? new DataInputStream(is) : null; + origOs = os; + this.os = os != null ? new DataOutputStream(os) : null; + } + + public Transport() { + this(null, null); + } + + void release() { + origIs = is = null; + origOs = os = null; + } + + public byte readByte() throws TransportException { + try { + if (baudrateMeter != null) baudrateMeter.count(1); + return is.readByte(); + } catch (EOFException e) { + throw new ClosedConnectionException(e); + } catch (IOException e) { + throw new TransportException("Cannot read byte", e); + } + + } + + public void setBaudrateMeter(BaudrateMeter baudrateMeter) { + this.baudrateMeter = baudrateMeter; + } + + public int readUInt8() throws TransportException { + return readByte() & 0x0ff; + } + + public int readUInt16() throws TransportException { + return readInt16() & 0x0ffff; + } + + public short readInt16() throws TransportException { + try { + if (baudrateMeter != null) baudrateMeter.count(2); + return is.readShort(); + } catch (EOFException e) { + throw new ClosedConnectionException(e); + } catch (IOException e) { + throw new TransportException("Cannot read int16", e); + } + } + + public long readUInt32() throws TransportException { + return readInt32() & 0xffffffffL; + } + + public int readInt32() throws TransportException { + try { + if (baudrateMeter != null) baudrateMeter.count(4); + return is.readInt(); + } catch (EOFException e) { + throw new ClosedConnectionException(e); + } catch (IOException e) { + throw new TransportException("Cannot read int32", e); + } + } + + public long readInt64() throws TransportException { + try { + if (baudrateMeter != null) baudrateMeter.count(8); + return is.readLong(); + } catch (EOFException e) { + throw new ClosedConnectionException(e); + } catch (IOException e) { + throw new TransportException("Cannot read int32", e); + } + } + + /** + * Read string by it length. + * Use this method only when sure no character accept ASCII will be read. + * Use readBytes and character encoding conversion instead. + * + * @return String read + */ + public String readString(int length) throws TransportException { +// return new String(readBytes(length), ISO_8859_1); + return stringWithBytesAndCharset(readBytes(length)); + } + + /** + * Read 32-bit string length and then string themself by it length + * Use this method only when sure no character accept ASCII will be read. + * Use readBytes and character encoding conversion instead or {@link #readUtf8String} method + * when utf-8 encoding needed. + * + * @return String read + * @throws TransportException + */ + public String readString() throws TransportException { + // unset most significant (sign) bit 'cause InputStream#readFully reads + // [int] length bytes from stream. Change when really need read string more + // than 2147483647 bytes length + int length = readInt32() & Integer.MAX_VALUE; + return readString(length); + } + + /** + * Read 32-bit string length and then string themself by it length + * Assume UTF-8 character encoding used + * + * @return String read + * @throws TransportException + */ + public String readUtf8String() throws TransportException { + // unset most significant (sign) bit 'cause InputStream#readFully reads + // [int] length bytes from stream. Change when really need read string more + // than 2147483647 bytes length + int length = readInt32() & Integer.MAX_VALUE; + return new String(readBytes(length), UTF8); + } + + /** + * Read @code{length} byte array + * Create byte array with length of @code{length}, read @code{length} bytes and return the array + * + * @param length + * @return byte array which contains the data read + * @throws TransportException + */ + public byte[] readBytes(int length) throws TransportException { + try { + return is.readNBytes(length); + } catch (IOException e) { + throw new TransportException(e.getMessage(), e); + } + } + + public byte[] readBytes(byte[] b, int offset, int length) throws TransportException { + try { + is.readFully(b, offset, length); + if (baudrateMeter != null) { + baudrateMeter.count(length); + } + return b; + } catch (EOFException e) { + throw new ClosedConnectionException(e); + } catch (IOException e) { + throw new TransportException("Cannot read " + length + " bytes array", e); + } + } + + public void skip(int length) throws TransportException { + try { + int rest = length; + do { + rest -= is.skipBytes(rest); + } while (rest > 0); + if (baudrateMeter != null) { + baudrateMeter.count(length); + } + } catch (EOFException e) { + throw new ClosedConnectionException(e); + } catch (IOException e) { + throw new TransportException("Cannot skip " + length + " bytes", e); + } + } + + private void checkForOutputInit() throws TransportException { + if (null == os) throw new TransportException("Uninitialized writer"); + } + + public Transport flush() throws TransportException { + checkForOutputInit(); + try { + os.flush(); + } catch (IOException e) { + throw new TransportException("Cannot flush output stream", e); + } + return this; + } + + public Transport writeByte(int b) throws TransportException { + return write((byte) (b & 0xff)); + } + + public Transport write(byte b) throws TransportException { + checkForOutputInit(); + try { + os.writeByte(b); + } catch (IOException e) { + throw new TransportException("Cannot write byte", e); + } + return this; + } + + public Transport writeInt16(int sh) throws TransportException { + return write((short) (sh & 0xffff)); + } + + public Transport write(short sh) throws TransportException { + checkForOutputInit(); + try { + os.writeShort(sh); + } catch (IOException e) { + throw new TransportException("Cannot write short", e); + } + return this; + } + + public Transport writeInt32(int i) throws TransportException { + return write(i); + } + + public Transport writeUInt32(long i) throws TransportException { + return write((int) i & 0xffffffff); + } + + public Transport writeInt64(long i) throws TransportException { + checkForOutputInit(); + try { + os.writeLong(i); + } catch (IOException e) { + throw new TransportException("Cannot write long", e); + } + return this; + } + + public Transport write(int i) throws TransportException { + checkForOutputInit(); + try { + os.writeInt(i); + } catch (IOException e) { + throw new TransportException("Cannot write int", e); + } + return this; + } + + public Transport write(byte[] b) throws TransportException { + return write(b, 0, b.length); + } + + public Transport write(byte[] b, int length) throws TransportException { + return write(b, 0, length); + } + + public Transport write(byte[] b, int offset, int length) throws TransportException { + checkForOutputInit(); + try { + os.write(b, offset, length <= b.length ? length : b.length); + } catch (IOException e) { + throw new TransportException("Cannot write " + length + " bytes", e); + } + return this; + } + + public void setOutputStreamTo(OutputStream os) { + this.os = new DataOutputStream(os); + } + + public Transport zero(int count) throws TransportException { + while (count-- > 0) { + writeByte(0); + } + return this; + } + + private String stringWithBytesAndCharset(byte[] bytes) { + String result; + try { + result = new String(bytes, ISO_8859_1); + } catch (NoSuchMethodError error) { + try { + result = new String(bytes, ISO_8859_1.name()); + } catch (UnsupportedEncodingException e) { + result = null; + } + } + return result; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/utils/Keymap.java b/plugins/vnc/src/main/java/com/glavsoft/utils/Keymap.java new file mode 100644 index 0000000..6ee9849 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/utils/Keymap.java @@ -0,0 +1,904 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.utils; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author dime at tightvnc.com + */ +public class Keymap { + + public static final int K_F1 = 0xffbe; + public static final int K_F2 = 0xffbf; + public static final int K_F3 = 0xffc0; + public static final int K_F4 = 0xffc1; + public static final int K_F5 = 0xffc2; + public static final int K_F6 = 0xffc3; + public static final int K_F7 = 0xffc4; + public static final int K_F8 = 0xffc5; + public static final int K_F9 = 0xffc6; + public static final int K_F10 = 0xffc7; + public static final int K_F11 = 0xffc8; + public static final int K_F12 = 0xffc9; + public static final int K_INSERT = 0xff63; + public static final int K_DELETE = 0xffff; + public static final int K_HOME = 0xff50; + public static final int K_END = 0xff57; + public static final int K_PAGE_DOWN = 0xff56; + public static final int K_PAGE_UP = 0xff55; + public static final int K_DOWN = 0xff54; + public static final int K_RIGHT = 0xff53; + public static final int K_UP = 0xff52; + public static final int K_LEFT = 0xff51; + public static final int K_ESCAPE = 0xff1b; + public static final int K_ENTER = 0xff0d; + public static final int K_TAB = 0xff09; + public static final int K_BACK_SPACE = 0xff08; + public static final int K_ALT_LEFT = 0xffe9; + public static final int K_META_LEFT = 0xffe7; + public static final int K_SHIFT_LEFT = 0xffe1; + public static final int K_CTRL_LEFT = 0xffe3; + public static final int K_SUPER_LEFT = 0xffeb; + public static final int K_HYPER_LEFT = 0xffed; + + public static final int K_KP_SPACE = 0xFF80; + public static final int K_KP_TAB = 0xFF89; + public static final int K_KP_ENTER = 0xFF8D; + public static final int K_KP_F1 = 0xFF91; + public static final int K_KP_F2 = 0xFF92; + public static final int K_KP_F3 = 0xFF93; + public static final int K_KP_F4 = 0xFF94; + public static final int K_KP_HOME = 0xFF95; + public static final int K_KP_LEFT = 0xFF96; + public static final int K_KP_UP = 0xFF97; + public static final int K_KP_RIGHT = 0xFF98; + public static final int K_KP_DOWN = 0xFF99; + public static final int K_KP_PRIOR = 0xFF9A; + public static final int K_KP_PAGE_UP = 0xFF9A; + public static final int K_KP_NEXT = 0xFF9B; + public static final int K_KP_PAGE_DOWN = 0xFF9B; + public static final int K_KP_END = 0xFF9C; + public static final int K_KP_BEGIN = 0xFF9D; + public static final int K_KP_INSERT = 0xFF9E; + public static final int K_KP_DELETE = 0xFF9F; + public static final int K_KP_EQUAL = 0xFFBD; + + public static final int K_KP_MULTIPLY = 0xFFAA; + public static final int K_KP_ADD = 0xFFAB; + public static final int K_KP_SEPARATOR = 0xFFAC; /* separator, often comma */ + public static final int K_KP_SUBTRACT = 0xFFAD; + public static final int K_KP_DECIMAL = 0xFFAE; + public static final int K_KP_DIVIDE = 0xFFAF; + + public static final int K_KP_0 = 0xFFB0; + public static final int K_KP_1 = 0xFFB1; + public static final int K_KP_2 = 0xFFB2; + public static final int K_KP_3 = 0xFFB3; + public static final int K_KP_4 = 0xFFB4; + public static final int K_KP_5 = 0xFFB5; + public static final int K_KP_6 = 0xFFB6; + public static final int K_KP_7 = 0xFFB7; + public static final int K_KP_8 = 0xFFB8; + public static final int K_KP_9 = 0xFFB9; + + private static Map keyMap = toMap(new int[][] { + // X Unicode + { 0x01a1, 0x0104 }, /* Aogonek LATIN CAPITAL LETTER A WITH OGONEK */ + { 0x01a2, 0x02d8 }, /* breve BREVE */ + { 0x01a3, 0x0141 }, /* Lstroke LATIN CAPITAL LETTER L WITH STROKE */ + { 0x01a5, 0x013d }, /* Lcaron LATIN CAPITAL LETTER L WITH CARON */ + { 0x01a6, 0x015a }, /* Sacute LATIN CAPITAL LETTER S WITH ACUTE */ + { 0x01a9, 0x0160 }, /* Scaron LATIN CAPITAL LETTER S WITH CARON */ + { 0x01aa, 0x015e }, /* Scedilla LATIN CAPITAL LETTER S WITH CEDILLA */ + { 0x01ab, 0x0164 }, /* Tcaron LATIN CAPITAL LETTER T WITH CARON */ + { 0x01ac, 0x0179 }, /* Zacute LATIN CAPITAL LETTER Z WITH ACUTE */ + { 0x01ae, 0x017d }, /* Zcaron LATIN CAPITAL LETTER Z WITH CARON */ + { 0x01af, 0x017b }, /* Zabovedot LATIN CAPITAL LETTER Z WITH DOT ABOVE */ + { 0x01b1, 0x0105 }, /* aogonek LATIN SMALL LETTER A WITH OGONEK */ + { 0x01b2, 0x02db }, /* ogonek OGONEK */ + { 0x01b3, 0x0142 }, /* lstroke LATIN SMALL LETTER L WITH STROKE */ + { 0x01b5, 0x013e }, /* lcaron LATIN SMALL LETTER L WITH CARON */ + { 0x01b6, 0x015b }, /* sacute LATIN SMALL LETTER S WITH ACUTE */ + { 0x01b7, 0x02c7 }, /* caron CARON */ + { 0x01b9, 0x0161 }, /* scaron LATIN SMALL LETTER S WITH CARON */ + { 0x01ba, 0x015f }, /* scedilla LATIN SMALL LETTER S WITH CEDILLA */ + { 0x01bb, 0x0165 }, /* tcaron LATIN SMALL LETTER T WITH CARON */ + { 0x01bc, 0x017a }, /* zacute LATIN SMALL LETTER Z WITH ACUTE */ + { 0x01bd, 0x02dd }, /* doubleacute DOUBLE ACUTE ACCENT */ + { 0x01be, 0x017e }, /* zcaron LATIN SMALL LETTER Z WITH CARON */ + { 0x01bf, 0x017c }, /* zabovedot LATIN SMALL LETTER Z WITH DOT ABOVE */ + { 0x01c0, 0x0154 }, /* Racute LATIN CAPITAL LETTER R WITH ACUTE */ + { 0x01c3, 0x0102 }, /* Abreve LATIN CAPITAL LETTER A WITH BREVE */ + { 0x01c5, 0x0139 }, /* Lacute LATIN CAPITAL LETTER L WITH ACUTE */ + { 0x01c6, 0x0106 }, /* Cacute LATIN CAPITAL LETTER C WITH ACUTE */ + { 0x01c8, 0x010c }, /* Ccaron LATIN CAPITAL LETTER C WITH CARON */ + { 0x01ca, 0x0118 }, /* Eogonek LATIN CAPITAL LETTER E WITH OGONEK */ + { 0x01cc, 0x011a }, /* Ecaron LATIN CAPITAL LETTER E WITH CARON */ + { 0x01cf, 0x010e }, /* Dcaron LATIN CAPITAL LETTER D WITH CARON */ + { 0x01d0, 0x0110 }, /* Dstroke LATIN CAPITAL LETTER D WITH STROKE */ + { 0x01d1, 0x0143 }, /* Nacute LATIN CAPITAL LETTER N WITH ACUTE */ + { 0x01d2, 0x0147 }, /* Ncaron LATIN CAPITAL LETTER N WITH CARON */ + { 0x01d5, 0x0150 }, /* Odoubleacute LATIN CAPITAL LETTER O WITH DOUBLE ACUTE */ + { 0x01d8, 0x0158 }, /* Rcaron LATIN CAPITAL LETTER R WITH CARON */ + { 0x01d9, 0x016e }, /* Uring LATIN CAPITAL LETTER U WITH RING ABOVE */ + { 0x01db, 0x0170 }, /* Udoubleacute LATIN CAPITAL LETTER U WITH DOUBLE ACUTE */ + { 0x01de, 0x0162 }, /* Tcedilla LATIN CAPITAL LETTER T WITH CEDILLA */ + { 0x01e0, 0x0155 }, /* racute LATIN SMALL LETTER R WITH ACUTE */ + { 0x01e3, 0x0103 }, /* abreve LATIN SMALL LETTER A WITH BREVE */ + { 0x01e5, 0x013a }, /* lacute LATIN SMALL LETTER L WITH ACUTE */ + { 0x01e6, 0x0107 }, /* cacute LATIN SMALL LETTER C WITH ACUTE */ + { 0x01e8, 0x010d }, /* ccaron LATIN SMALL LETTER C WITH CARON */ + { 0x01ea, 0x0119 }, /* eogonek LATIN SMALL LETTER E WITH OGONEK */ + { 0x01ec, 0x011b }, /* ecaron LATIN SMALL LETTER E WITH CARON */ + { 0x01ef, 0x010f }, /* dcaron LATIN SMALL LETTER D WITH CARON */ + { 0x01f0, 0x0111 }, /* dstroke LATIN SMALL LETTER D WITH STROKE */ + { 0x01f1, 0x0144 }, /* nacute LATIN SMALL LETTER N WITH ACUTE */ + { 0x01f2, 0x0148 }, /* ncaron LATIN SMALL LETTER N WITH CARON */ + { 0x01f5, 0x0151 }, /* odoubleacute LATIN SMALL LETTER O WITH DOUBLE ACUTE */ + { 0x01f8, 0x0159 }, /* rcaron LATIN SMALL LETTER R WITH CARON */ + { 0x01f9, 0x016f }, /* uring LATIN SMALL LETTER U WITH RING ABOVE */ + { 0x01fb, 0x0171 }, /* udoubleacute LATIN SMALL LETTER U WITH DOUBLE ACUTE */ + { 0x01fe, 0x0163 }, /* tcedilla LATIN SMALL LETTER T WITH CEDILLA */ + { 0x01ff, 0x02d9 }, /* abovedot DOT ABOVE */ + { 0x02a1, 0x0126 }, /* Hstroke LATIN CAPITAL LETTER H WITH STROKE */ + { 0x02a6, 0x0124 }, /* Hcircumflex LATIN CAPITAL LETTER H WITH CIRCUMFLEX */ + { 0x02a9, 0x0130 }, /* Iabovedot LATIN CAPITAL LETTER I WITH DOT ABOVE */ + { 0x02ab, 0x011e }, /* Gbreve LATIN CAPITAL LETTER G WITH BREVE */ + { 0x02ac, 0x0134 }, /* Jcircumflex LATIN CAPITAL LETTER J WITH CIRCUMFLEX */ + { 0x02b1, 0x0127 }, /* hstroke LATIN SMALL LETTER H WITH STROKE */ + { 0x02b6, 0x0125 }, /* hcircumflex LATIN SMALL LETTER H WITH CIRCUMFLEX */ + { 0x02b9, 0x0131 }, /* idotless LATIN SMALL LETTER DOTLESS I */ + { 0x02bb, 0x011f }, /* gbreve LATIN SMALL LETTER G WITH BREVE */ + { 0x02bc, 0x0135 }, /* jcircumflex LATIN SMALL LETTER J WITH CIRCUMFLEX */ + { 0x02c5, 0x010a }, /* Cabovedot LATIN CAPITAL LETTER C WITH DOT ABOVE */ + { 0x02c6, 0x0108 }, /* Ccircumflex LATIN CAPITAL LETTER C WITH CIRCUMFLEX */ + { 0x02d5, 0x0120 }, /* Gabovedot LATIN CAPITAL LETTER G WITH DOT ABOVE */ + { 0x02d8, 0x011c }, /* Gcircumflex LATIN CAPITAL LETTER G WITH CIRCUMFLEX */ + { 0x02dd, 0x016c }, /* Ubreve LATIN CAPITAL LETTER U WITH BREVE */ + { 0x02de, 0x015c }, /* Scircumflex LATIN CAPITAL LETTER S WITH CIRCUMFLEX */ + { 0x02e5, 0x010b }, /* cabovedot LATIN SMALL LETTER C WITH DOT ABOVE */ + { 0x02e6, 0x0109 }, /* ccircumflex LATIN SMALL LETTER C WITH CIRCUMFLEX */ + { 0x02f5, 0x0121 }, /* gabovedot LATIN SMALL LETTER G WITH DOT ABOVE */ + { 0x02f8, 0x011d }, /* gcircumflex LATIN SMALL LETTER G WITH CIRCUMFLEX */ + { 0x02fd, 0x016d }, /* ubreve LATIN SMALL LETTER U WITH BREVE */ + { 0x02fe, 0x015d }, /* scircumflex LATIN SMALL LETTER S WITH CIRCUMFLEX */ + { 0x03a2, 0x0138 }, /* kra LATIN SMALL LETTER KRA */ + { 0x03a3, 0x0156 }, /* Rcedilla LATIN CAPITAL LETTER R WITH CEDILLA */ + { 0x03a5, 0x0128 }, /* Itilde LATIN CAPITAL LETTER I WITH TILDE */ + { 0x03a6, 0x013b }, /* Lcedilla LATIN CAPITAL LETTER L WITH CEDILLA */ + { 0x03aa, 0x0112 }, /* Emacron LATIN CAPITAL LETTER E WITH MACRON */ + { 0x03ab, 0x0122 }, /* Gcedilla LATIN CAPITAL LETTER G WITH CEDILLA */ + { 0x03ac, 0x0166 }, /* Tslash LATIN CAPITAL LETTER T WITH STROKE */ + { 0x03b3, 0x0157 }, /* rcedilla LATIN SMALL LETTER R WITH CEDILLA */ + { 0x03b5, 0x0129 }, /* itilde LATIN SMALL LETTER I WITH TILDE */ + { 0x03b6, 0x013c }, /* lcedilla LATIN SMALL LETTER L WITH CEDILLA */ + { 0x03ba, 0x0113 }, /* emacron LATIN SMALL LETTER E WITH MACRON */ + { 0x03bb, 0x0123 }, /* gcedilla LATIN SMALL LETTER G WITH CEDILLA */ + { 0x03bc, 0x0167 }, /* tslash LATIN SMALL LETTER T WITH STROKE */ + { 0x03bd, 0x014a }, /* ENG LATIN CAPITAL LETTER ENG */ + { 0x03bf, 0x014b }, /* eng LATIN SMALL LETTER ENG */ + { 0x03c0, 0x0100 }, /* Amacron LATIN CAPITAL LETTER A WITH MACRON */ + { 0x03c7, 0x012e }, /* Iogonek LATIN CAPITAL LETTER I WITH OGONEK */ + { 0x03cc, 0x0116 }, /* Eabovedot LATIN CAPITAL LETTER E WITH DOT ABOVE */ + { 0x03cf, 0x012a }, /* Imacron LATIN CAPITAL LETTER I WITH MACRON */ + { 0x03d1, 0x0145 }, /* Ncedilla LATIN CAPITAL LETTER N WITH CEDILLA */ + { 0x03d2, 0x014c }, /* Omacron LATIN CAPITAL LETTER O WITH MACRON */ + { 0x03d3, 0x0136 }, /* Kcedilla LATIN CAPITAL LETTER K WITH CEDILLA */ + { 0x03d9, 0x0172 }, /* Uogonek LATIN CAPITAL LETTER U WITH OGONEK */ + { 0x03dd, 0x0168 }, /* Utilde LATIN CAPITAL LETTER U WITH TILDE */ + { 0x03de, 0x016a }, /* Umacron LATIN CAPITAL LETTER U WITH MACRON */ + { 0x03e0, 0x0101 }, /* amacron LATIN SMALL LETTER A WITH MACRON */ + { 0x03e7, 0x012f }, /* iogonek LATIN SMALL LETTER I WITH OGONEK */ + { 0x03ec, 0x0117 }, /* eabovedot LATIN SMALL LETTER E WITH DOT ABOVE */ + { 0x03ef, 0x012b }, /* imacron LATIN SMALL LETTER I WITH MACRON */ + { 0x03f1, 0x0146 }, /* ncedilla LATIN SMALL LETTER N WITH CEDILLA */ + { 0x03f2, 0x014d }, /* omacron LATIN SMALL LETTER O WITH MACRON */ + { 0x03f3, 0x0137 }, /* kcedilla LATIN SMALL LETTER K WITH CEDILLA */ + { 0x03f9, 0x0173 }, /* uogonek LATIN SMALL LETTER U WITH OGONEK */ + { 0x03fd, 0x0169 }, /* utilde LATIN SMALL LETTER U WITH TILDE */ + { 0x03fe, 0x016b }, /* umacron LATIN SMALL LETTER U WITH MACRON */ + { 0x047e, 0x203e }, /* overline OVERLINE */ + { 0x04a1, 0x3002 }, /* kana_fullstop IDEOGRAPHIC FULL STOP */ + { 0x04a2, 0x300c }, /* kana_openingbracket LEFT CORNER BRACKET */ + { 0x04a3, 0x300d }, /* kana_closingbracket RIGHT CORNER BRACKET */ + { 0x04a4, 0x3001 }, /* kana_comma IDEOGRAPHIC COMMA */ + { 0x04a5, 0x30fb }, /* kana_conjunctive KATAKANA MIDDLE DOT */ + { 0x04a6, 0x30f2 }, /* kana_WO KATAKANA LETTER WO */ + { 0x04a7, 0x30a1 }, /* kana_a KATAKANA LETTER SMALL A */ + { 0x04a8, 0x30a3 }, /* kana_i KATAKANA LETTER SMALL I */ + { 0x04a9, 0x30a5 }, /* kana_u KATAKANA LETTER SMALL U */ + { 0x04aa, 0x30a7 }, /* kana_e KATAKANA LETTER SMALL E */ + { 0x04ab, 0x30a9 }, /* kana_o KATAKANA LETTER SMALL O */ + { 0x04ac, 0x30e3 }, /* kana_ya KATAKANA LETTER SMALL YA */ + { 0x04ad, 0x30e5 }, /* kana_yu KATAKANA LETTER SMALL YU */ + { 0x04ae, 0x30e7 }, /* kana_yo KATAKANA LETTER SMALL YO */ + { 0x04af, 0x30c3 }, /* kana_tsu KATAKANA LETTER SMALL TU */ + { 0x04b0, 0x30fc }, /* prolongedsound KATAKANA-HIRAGANA PROLONGED SOUND MARK */ + { 0x04b1, 0x30a2 }, /* kana_A KATAKANA LETTER A */ + { 0x04b2, 0x30a4 }, /* kana_I KATAKANA LETTER I */ + { 0x04b3, 0x30a6 }, /* kana_U KATAKANA LETTER U */ + { 0x04b4, 0x30a8 }, /* kana_E KATAKANA LETTER E */ + { 0x04b5, 0x30aa }, /* kana_O KATAKANA LETTER O */ + { 0x04b6, 0x30ab }, /* kana_KA KATAKANA LETTER KA */ + { 0x04b7, 0x30ad }, /* kana_KI KATAKANA LETTER KI */ + { 0x04b8, 0x30af }, /* kana_KU KATAKANA LETTER KU */ + { 0x04b9, 0x30b1 }, /* kana_KE KATAKANA LETTER KE */ + { 0x04ba, 0x30b3 }, /* kana_KO KATAKANA LETTER KO */ + { 0x04bb, 0x30b5 }, /* kana_SA KATAKANA LETTER SA */ + { 0x04bc, 0x30b7 }, /* kana_SHI KATAKANA LETTER SI */ + { 0x04bd, 0x30b9 }, /* kana_SU KATAKANA LETTER SU */ + { 0x04be, 0x30bb }, /* kana_SE KATAKANA LETTER SE */ + { 0x04bf, 0x30bd }, /* kana_SO KATAKANA LETTER SO */ + { 0x04c0, 0x30bf }, /* kana_TA KATAKANA LETTER TA */ + { 0x04c1, 0x30c1 }, /* kana_CHI KATAKANA LETTER TI */ + { 0x04c2, 0x30c4 }, /* kana_TSU KATAKANA LETTER TU */ + { 0x04c3, 0x30c6 }, /* kana_TE KATAKANA LETTER TE */ + { 0x04c4, 0x30c8 }, /* kana_TO KATAKANA LETTER TO */ + { 0x04c5, 0x30ca }, /* kana_NA KATAKANA LETTER NA */ + { 0x04c6, 0x30cb }, /* kana_NI KATAKANA LETTER NI */ + { 0x04c7, 0x30cc }, /* kana_NU KATAKANA LETTER NU */ + { 0x04c8, 0x30cd }, /* kana_NE KATAKANA LETTER NE */ + { 0x04c9, 0x30ce }, /* kana_NO KATAKANA LETTER NO */ + { 0x04ca, 0x30cf }, /* kana_HA KATAKANA LETTER HA */ + { 0x04cb, 0x30d2 }, /* kana_HI KATAKANA LETTER HI */ + { 0x04cc, 0x30d5 }, /* kana_FU KATAKANA LETTER HU */ + { 0x04cd, 0x30d8 }, /* kana_HE KATAKANA LETTER HE */ + { 0x04ce, 0x30db }, /* kana_HO KATAKANA LETTER HO */ + { 0x04cf, 0x30de }, /* kana_MA KATAKANA LETTER MA */ + { 0x04d0, 0x30df }, /* kana_MI KATAKANA LETTER MI */ + { 0x04d1, 0x30e0 }, /* kana_MU KATAKANA LETTER MU */ + { 0x04d2, 0x30e1 }, /* kana_ME KATAKANA LETTER ME */ + { 0x04d3, 0x30e2 }, /* kana_MO KATAKANA LETTER MO */ + { 0x04d4, 0x30e4 }, /* kana_YA KATAKANA LETTER YA */ + { 0x04d5, 0x30e6 }, /* kana_YU KATAKANA LETTER YU */ + { 0x04d6, 0x30e8 }, /* kana_YO KATAKANA LETTER YO */ + { 0x04d7, 0x30e9 }, /* kana_RA KATAKANA LETTER RA */ + { 0x04d8, 0x30ea }, /* kana_RI KATAKANA LETTER RI */ + { 0x04d9, 0x30eb }, /* kana_RU KATAKANA LETTER RU */ + { 0x04da, 0x30ec }, /* kana_RE KATAKANA LETTER RE */ + { 0x04db, 0x30ed }, /* kana_RO KATAKANA LETTER RO */ + { 0x04dc, 0x30ef }, /* kana_WA KATAKANA LETTER WA */ + { 0x04dd, 0x30f3 }, /* kana_N KATAKANA LETTER N */ + { 0x04de, 0x309b }, /* voicedsound KATAKANA-HIRAGANA VOICED SOUND MARK */ + { 0x04df, 0x309c }, /* semivoicedsound KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK */ + { 0x05ac, 0x060c }, /* Arabic_comma ARABIC COMMA */ + { 0x05bb, 0x061b }, /* Arabic_semicolon ARABIC SEMICOLON */ + { 0x05bf, 0x061f }, /* Arabic_question_mark ARABIC QUESTION MARK */ + { 0x05c1, 0x0621 }, /* Arabic_hamza ARABIC LETTER HAMZA */ + { 0x05c2, 0x0622 }, /* Arabic_maddaonalef ARABIC LETTER ALEF WITH MADDA ABOVE */ + { 0x05c3, 0x0623 }, /* Arabic_hamzaonalef ARABIC LETTER ALEF WITH HAMZA ABOVE */ + { 0x05c4, 0x0624 }, /* Arabic_hamzaonwaw ARABIC LETTER WAW WITH HAMZA ABOVE */ + { 0x05c5, 0x0625 }, /* Arabic_hamzaunderalef ARABIC LETTER ALEF WITH HAMZA BELOW */ + { 0x05c6, 0x0626 }, /* Arabic_hamzaonyeh ARABIC LETTER YEH WITH HAMZA ABOVE */ + { 0x05c7, 0x0627 }, /* Arabic_alef ARABIC LETTER ALEF */ + { 0x05c8, 0x0628 }, /* Arabic_beh ARABIC LETTER BEH */ + { 0x05c9, 0x0629 }, /* Arabic_tehmarbuta ARABIC LETTER TEH MARBUTA */ + { 0x05ca, 0x062a }, /* Arabic_teh ARABIC LETTER TEH */ + { 0x05cb, 0x062b }, /* Arabic_theh ARABIC LETTER THEH */ + { 0x05cc, 0x062c }, /* Arabic_jeem ARABIC LETTER JEEM */ + { 0x05cd, 0x062d }, /* Arabic_hah ARABIC LETTER HAH */ + { 0x05ce, 0x062e }, /* Arabic_khah ARABIC LETTER KHAH */ + { 0x05cf, 0x062f }, /* Arabic_dal ARABIC LETTER DAL */ + { 0x05d0, 0x0630 }, /* Arabic_thal ARABIC LETTER THAL */ + { 0x05d1, 0x0631 }, /* Arabic_ra ARABIC LETTER REH */ + { 0x05d2, 0x0632 }, /* Arabic_zain ARABIC LETTER ZAIN */ + { 0x05d3, 0x0633 }, /* Arabic_seen ARABIC LETTER SEEN */ + { 0x05d4, 0x0634 }, /* Arabic_sheen ARABIC LETTER SHEEN */ + { 0x05d5, 0x0635 }, /* Arabic_sad ARABIC LETTER SAD */ + { 0x05d6, 0x0636 }, /* Arabic_dad ARABIC LETTER DAD */ + { 0x05d7, 0x0637 }, /* Arabic_tah ARABIC LETTER TAH */ + { 0x05d8, 0x0638 }, /* Arabic_zah ARABIC LETTER ZAH */ + { 0x05d9, 0x0639 }, /* Arabic_ain ARABIC LETTER AIN */ + { 0x05da, 0x063a }, /* Arabic_ghain ARABIC LETTER GHAIN */ + { 0x05e0, 0x0640 }, /* Arabic_tatweel ARABIC TATWEEL */ + { 0x05e1, 0x0641 }, /* Arabic_feh ARABIC LETTER FEH */ + { 0x05e2, 0x0642 }, /* Arabic_qaf ARABIC LETTER QAF */ + { 0x05e3, 0x0643 }, /* Arabic_kaf ARABIC LETTER KAF */ + { 0x05e4, 0x0644 }, /* Arabic_lam ARABIC LETTER LAM */ + { 0x05e5, 0x0645 }, /* Arabic_meem ARABIC LETTER MEEM */ + { 0x05e6, 0x0646 }, /* Arabic_noon ARABIC LETTER NOON */ + { 0x05e7, 0x0647 }, /* Arabic_ha ARABIC LETTER HEH */ + { 0x05e8, 0x0648 }, /* Arabic_waw ARABIC LETTER WAW */ + { 0x05e9, 0x0649 }, /* Arabic_alefmaksura ARABIC LETTER ALEF MAKSURA */ + { 0x05ea, 0x064a }, /* Arabic_yeh ARABIC LETTER YEH */ + { 0x05eb, 0x064b }, /* Arabic_fathatan ARABIC FATHATAN */ + { 0x05ec, 0x064c }, /* Arabic_dammatan ARABIC DAMMATAN */ + { 0x05ed, 0x064d }, /* Arabic_kasratan ARABIC KASRATAN */ + { 0x05ee, 0x064e }, /* Arabic_fatha ARABIC FATHA */ + { 0x05ef, 0x064f }, /* Arabic_damma ARABIC DAMMA */ + { 0x05f0, 0x0650 }, /* Arabic_kasra ARABIC KASRA */ + { 0x05f1, 0x0651 }, /* Arabic_shadda ARABIC SHADDA */ + { 0x05f2, 0x0652 }, /* Arabic_sukun ARABIC SUKUN */ + { 0x06a1, 0x0452 }, /* Serbian_dje CYRILLIC SMALL LETTER DJE */ + { 0x06a2, 0x0453 }, /* Macedonia_gje CYRILLIC SMALL LETTER GJE */ + { 0x06a3, 0x0451 }, /* Cyrillic_io CYRILLIC SMALL LETTER IO */ + { 0x06a4, 0x0454 }, /* Ukrainian_ie CYRILLIC SMALL LETTER UKRAINIAN IE */ + { 0x06a5, 0x0455 }, /* Macedonia_dse CYRILLIC SMALL LETTER DZE */ + { 0x06a6, 0x0456 }, /* Ukrainian_i CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I */ + { 0x06a7, 0x0457 }, /* Ukrainian_yi CYRILLIC SMALL LETTER YI */ + { 0x06a8, 0x0458 }, /* Cyrillic_je CYRILLIC SMALL LETTER JE */ + { 0x06a9, 0x0459 }, /* Cyrillic_lje CYRILLIC SMALL LETTER LJE */ + { 0x06aa, 0x045a }, /* Cyrillic_nje CYRILLIC SMALL LETTER NJE */ + { 0x06ab, 0x045b }, /* Serbian_tshe CYRILLIC SMALL LETTER TSHE */ + { 0x06ac, 0x045c }, /* Macedonia_kje CYRILLIC SMALL LETTER KJE */ + { 0x06ae, 0x045e }, /* Byelorussian_shortu CYRILLIC SMALL LETTER SHORT U */ + { 0x06af, 0x045f }, /* Cyrillic_dzhe CYRILLIC SMALL LETTER DZHE */ + { 0x06b0, 0x2116 }, /* numerosign NUMERO SIGN */ + { 0x06b1, 0x0402 }, /* Serbian_DJE CYRILLIC CAPITAL LETTER DJE */ + { 0x06b2, 0x0403 }, /* Macedonia_GJE CYRILLIC CAPITAL LETTER GJE */ + { 0x06b3, 0x0401 }, /* Cyrillic_IO CYRILLIC CAPITAL LETTER IO */ + { 0x06b4, 0x0404 }, /* Ukrainian_IE CYRILLIC CAPITAL LETTER UKRAINIAN IE */ + { 0x06b5, 0x0405 }, /* Macedonia_DSE CYRILLIC CAPITAL LETTER DZE */ + { 0x06b6, 0x0406 }, /* Ukrainian_I CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I */ + { 0x06b7, 0x0407 }, /* Ukrainian_YI CYRILLIC CAPITAL LETTER YI */ + { 0x06b8, 0x0408 }, /* Cyrillic_JE CYRILLIC CAPITAL LETTER JE */ + { 0x06b9, 0x0409 }, /* Cyrillic_LJE CYRILLIC CAPITAL LETTER LJE */ + { 0x06ba, 0x040a }, /* Cyrillic_NJE CYRILLIC CAPITAL LETTER NJE */ + { 0x06bb, 0x040b }, /* Serbian_TSHE CYRILLIC CAPITAL LETTER TSHE */ + { 0x06bc, 0x040c }, /* Macedonia_KJE CYRILLIC CAPITAL LETTER KJE */ + { 0x06be, 0x040e }, /* Byelorussian_SHORTU CYRILLIC CAPITAL LETTER SHORT U */ + { 0x06bf, 0x040f }, /* Cyrillic_DZHE CYRILLIC CAPITAL LETTER DZHE */ + { 0x06c0, 0x044e }, /* Cyrillic_yu CYRILLIC SMALL LETTER YU */ + { 0x06c1, 0x0430 }, /* Cyrillic_a CYRILLIC SMALL LETTER A */ + { 0x06c2, 0x0431 }, /* Cyrillic_be CYRILLIC SMALL LETTER BE */ + { 0x06c3, 0x0446 }, /* Cyrillic_tse CYRILLIC SMALL LETTER TSE */ + { 0x06c4, 0x0434 }, /* Cyrillic_de CYRILLIC SMALL LETTER DE */ + { 0x06c5, 0x0435 }, /* Cyrillic_ie CYRILLIC SMALL LETTER IE */ + { 0x06c6, 0x0444 }, /* Cyrillic_ef CYRILLIC SMALL LETTER EF */ + { 0x06c7, 0x0433 }, /* Cyrillic_ghe CYRILLIC SMALL LETTER GHE */ + { 0x06c8, 0x0445 }, /* Cyrillic_ha CYRILLIC SMALL LETTER HA */ + { 0x06c9, 0x0438 }, /* Cyrillic_i CYRILLIC SMALL LETTER I */ + { 0x06ca, 0x0439 }, /* Cyrillic_shorti CYRILLIC SMALL LETTER SHORT I */ + { 0x06cb, 0x043a }, /* Cyrillic_ka CYRILLIC SMALL LETTER KA */ + { 0x06cc, 0x043b }, /* Cyrillic_el CYRILLIC SMALL LETTER EL */ + { 0x06cd, 0x043c }, /* Cyrillic_em CYRILLIC SMALL LETTER EM */ + { 0x06ce, 0x043d }, /* Cyrillic_en CYRILLIC SMALL LETTER EN */ + { 0x06cf, 0x043e }, /* Cyrillic_o CYRILLIC SMALL LETTER O */ + { 0x06d0, 0x043f }, /* Cyrillic_pe CYRILLIC SMALL LETTER PE */ + { 0x06d1, 0x044f }, /* Cyrillic_ya CYRILLIC SMALL LETTER YA */ + { 0x06d2, 0x0440 }, /* Cyrillic_er CYRILLIC SMALL LETTER ER */ + { 0x06d3, 0x0441 }, /* Cyrillic_es CYRILLIC SMALL LETTER ES */ + { 0x06d4, 0x0442 }, /* Cyrillic_te CYRILLIC SMALL LETTER TE */ + { 0x06d5, 0x0443 }, /* Cyrillic_u CYRILLIC SMALL LETTER U */ + { 0x06d6, 0x0436 }, /* Cyrillic_zhe CYRILLIC SMALL LETTER ZHE */ + { 0x06d7, 0x0432 }, /* Cyrillic_ve CYRILLIC SMALL LETTER VE */ + { 0x06d8, 0x044c }, /* Cyrillic_softsign CYRILLIC SMALL LETTER SOFT SIGN */ + { 0x06d9, 0x044b }, /* Cyrillic_yeru CYRILLIC SMALL LETTER YERU */ + { 0x06da, 0x0437 }, /* Cyrillic_ze CYRILLIC SMALL LETTER ZE */ + { 0x06db, 0x0448 }, /* Cyrillic_sha CYRILLIC SMALL LETTER SHA */ + { 0x06dc, 0x044d }, /* Cyrillic_e CYRILLIC SMALL LETTER E */ + { 0x06dd, 0x0449 }, /* Cyrillic_shcha CYRILLIC SMALL LETTER SHCHA */ + { 0x06de, 0x0447 }, /* Cyrillic_che CYRILLIC SMALL LETTER CHE */ + { 0x06df, 0x044a }, /* Cyrillic_hardsign CYRILLIC SMALL LETTER HARD SIGN */ + { 0x06e0, 0x042e }, /* Cyrillic_YU CYRILLIC CAPITAL LETTER YU */ + { 0x06e1, 0x0410 }, /* Cyrillic_A CYRILLIC CAPITAL LETTER A */ + { 0x06e2, 0x0411 }, /* Cyrillic_BE CYRILLIC CAPITAL LETTER BE */ + { 0x06e3, 0x0426 }, /* Cyrillic_TSE CYRILLIC CAPITAL LETTER TSE */ + { 0x06e4, 0x0414 }, /* Cyrillic_DE CYRILLIC CAPITAL LETTER DE */ + { 0x06e5, 0x0415 }, /* Cyrillic_IE CYRILLIC CAPITAL LETTER IE */ + { 0x06e6, 0x0424 }, /* Cyrillic_EF CYRILLIC CAPITAL LETTER EF */ + { 0x06e7, 0x0413 }, /* Cyrillic_GHE CYRILLIC CAPITAL LETTER GHE */ + { 0x06e8, 0x0425 }, /* Cyrillic_HA CYRILLIC CAPITAL LETTER HA */ + { 0x06e9, 0x0418 }, /* Cyrillic_I CYRILLIC CAPITAL LETTER I */ + { 0x06ea, 0x0419 }, /* Cyrillic_SHORTI CYRILLIC CAPITAL LETTER SHORT I */ + { 0x06eb, 0x041a }, /* Cyrillic_KA CYRILLIC CAPITAL LETTER KA */ + { 0x06ec, 0x041b }, /* Cyrillic_EL CYRILLIC CAPITAL LETTER EL */ + { 0x06ed, 0x041c }, /* Cyrillic_EM CYRILLIC CAPITAL LETTER EM */ + { 0x06ee, 0x041d }, /* Cyrillic_EN CYRILLIC CAPITAL LETTER EN */ + { 0x06ef, 0x041e }, /* Cyrillic_O CYRILLIC CAPITAL LETTER O */ + { 0x06f0, 0x041f }, /* Cyrillic_PE CYRILLIC CAPITAL LETTER PE */ + { 0x06f1, 0x042f }, /* Cyrillic_YA CYRILLIC CAPITAL LETTER YA */ + { 0x06f2, 0x0420 }, /* Cyrillic_ER CYRILLIC CAPITAL LETTER ER */ + { 0x06f3, 0x0421 }, /* Cyrillic_ES CYRILLIC CAPITAL LETTER ES */ + { 0x06f4, 0x0422 }, /* Cyrillic_TE CYRILLIC CAPITAL LETTER TE */ + { 0x06f5, 0x0423 }, /* Cyrillic_U CYRILLIC CAPITAL LETTER U */ + { 0x06f6, 0x0416 }, /* Cyrillic_ZHE CYRILLIC CAPITAL LETTER ZHE */ + { 0x06f7, 0x0412 }, /* Cyrillic_VE CYRILLIC CAPITAL LETTER VE */ + { 0x06f8, 0x042c }, /* Cyrillic_SOFTSIGN CYRILLIC CAPITAL LETTER SOFT SIGN */ + { 0x06f9, 0x042b }, /* Cyrillic_YERU CYRILLIC CAPITAL LETTER YERU */ + { 0x06fa, 0x0417 }, /* Cyrillic_ZE CYRILLIC CAPITAL LETTER ZE */ + { 0x06fb, 0x0428 }, /* Cyrillic_SHA CYRILLIC CAPITAL LETTER SHA */ + { 0x06fc, 0x042d }, /* Cyrillic_E CYRILLIC CAPITAL LETTER E */ + { 0x06fd, 0x0429 }, /* Cyrillic_SHCHA CYRILLIC CAPITAL LETTER SHCHA */ + { 0x06fe, 0x0427 }, /* Cyrillic_CHE CYRILLIC CAPITAL LETTER CHE */ + { 0x06ff, 0x042a }, /* Cyrillic_HARDSIGN CYRILLIC CAPITAL LETTER HARD SIGN */ + { 0x07a1, 0x0386 }, /* Greek_ALPHAaccent GREEK CAPITAL LETTER ALPHA WITH TONOS */ + { 0x07a2, 0x0388 }, /* Greek_EPSILONaccent GREEK CAPITAL LETTER EPSILON WITH TONOS */ + { 0x07a3, 0x0389 }, /* Greek_ETAaccent GREEK CAPITAL LETTER ETA WITH TONOS */ + { 0x07a4, 0x038a }, /* Greek_IOTAaccent GREEK CAPITAL LETTER IOTA WITH TONOS */ + { 0x07a5, 0x03aa }, /* Greek_IOTAdiaeresis GREEK CAPITAL LETTER IOTA WITH DIALYTIKA */ + { 0x07a7, 0x038c }, /* Greek_OMICRONaccent GREEK CAPITAL LETTER OMICRON WITH TONOS */ + { 0x07a8, 0x038e }, /* Greek_UPSILONaccent GREEK CAPITAL LETTER UPSILON WITH TONOS */ + { 0x07a9, 0x03ab }, /* Greek_UPSILONdieresis GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA */ + { 0x07ab, 0x038f }, /* Greek_OMEGAaccent GREEK CAPITAL LETTER OMEGA WITH TONOS */ + { 0x07ae, 0x0385 }, /* Greek_accentdieresis GREEK DIALYTIKA TONOS */ + { 0x07af, 0x2015 }, /* Greek_horizbar HORIZONTAL BAR */ + { 0x07b1, 0x03ac }, /* Greek_alphaaccent GREEK SMALL LETTER ALPHA WITH TONOS */ + { 0x07b2, 0x03ad }, /* Greek_epsilonaccent GREEK SMALL LETTER EPSILON WITH TONOS */ + { 0x07b3, 0x03ae }, /* Greek_etaaccent GREEK SMALL LETTER ETA WITH TONOS */ + { 0x07b4, 0x03af }, /* Greek_iotaaccent GREEK SMALL LETTER IOTA WITH TONOS */ + { 0x07b5, 0x03ca }, /* Greek_iotadieresis GREEK SMALL LETTER IOTA WITH DIALYTIKA */ + { 0x07b6, 0x0390 }, /* Greek_iotaaccentdieresis GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS */ + { 0x07b7, 0x03cc }, /* Greek_omicronaccent GREEK SMALL LETTER OMICRON WITH TONOS */ + { 0x07b8, 0x03cd }, /* Greek_upsilonaccent GREEK SMALL LETTER UPSILON WITH TONOS */ + { 0x07b9, 0x03cb }, /* Greek_upsilondieresis GREEK SMALL LETTER UPSILON WITH DIALYTIKA */ + { 0x07ba, 0x03b0 }, /* Greek_upsilonaccentdieresis GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS */ + { 0x07bb, 0x03ce }, /* Greek_omegaaccent GREEK SMALL LETTER OMEGA WITH TONOS */ + { 0x07c1, 0x0391 }, /* Greek_ALPHA GREEK CAPITAL LETTER ALPHA */ + { 0x07c2, 0x0392 }, /* Greek_BETA GREEK CAPITAL LETTER BETA */ + { 0x07c3, 0x0393 }, /* Greek_GAMMA GREEK CAPITAL LETTER GAMMA */ + { 0x07c4, 0x0394 }, /* Greek_DELTA GREEK CAPITAL LETTER DELTA */ + { 0x07c5, 0x0395 }, /* Greek_EPSILON GREEK CAPITAL LETTER EPSILON */ + { 0x07c6, 0x0396 }, /* Greek_ZETA GREEK CAPITAL LETTER ZETA */ + { 0x07c7, 0x0397 }, /* Greek_ETA GREEK CAPITAL LETTER ETA */ + { 0x07c8, 0x0398 }, /* Greek_THETA GREEK CAPITAL LETTER THETA */ + { 0x07c9, 0x0399 }, /* Greek_IOTA GREEK CAPITAL LETTER IOTA */ + { 0x07ca, 0x039a }, /* Greek_KAPPA GREEK CAPITAL LETTER KAPPA */ + { 0x07cb, 0x039b }, /* Greek_LAMBDA GREEK CAPITAL LETTER LAMDA */ + { 0x07cc, 0x039c }, /* Greek_MU GREEK CAPITAL LETTER MU */ + { 0x07cd, 0x039d }, /* Greek_NU GREEK CAPITAL LETTER NU */ + { 0x07ce, 0x039e }, /* Greek_XI GREEK CAPITAL LETTER XI */ + { 0x07cf, 0x039f }, /* Greek_OMICRON GREEK CAPITAL LETTER OMICRON */ + { 0x07d0, 0x03a0 }, /* Greek_PI GREEK CAPITAL LETTER PI */ + { 0x07d1, 0x03a1 }, /* Greek_RHO GREEK CAPITAL LETTER RHO */ + { 0x07d2, 0x03a3 }, /* Greek_SIGMA GREEK CAPITAL LETTER SIGMA */ + { 0x07d4, 0x03a4 }, /* Greek_TAU GREEK CAPITAL LETTER TAU */ + { 0x07d5, 0x03a5 }, /* Greek_UPSILON GREEK CAPITAL LETTER UPSILON */ + { 0x07d6, 0x03a6 }, /* Greek_PHI GREEK CAPITAL LETTER PHI */ + { 0x07d7, 0x03a7 }, /* Greek_CHI GREEK CAPITAL LETTER CHI */ + { 0x07d8, 0x03a8 }, /* Greek_PSI GREEK CAPITAL LETTER PSI */ + { 0x07d9, 0x03a9 }, /* Greek_OMEGA GREEK CAPITAL LETTER OMEGA */ + { 0x07e1, 0x03b1 }, /* Greek_alpha GREEK SMALL LETTER ALPHA */ + { 0x07e2, 0x03b2 }, /* Greek_beta GREEK SMALL LETTER BETA */ + { 0x07e3, 0x03b3 }, /* Greek_gamma GREEK SMALL LETTER GAMMA */ + { 0x07e4, 0x03b4 }, /* Greek_delta GREEK SMALL LETTER DELTA */ + { 0x07e5, 0x03b5 }, /* Greek_epsilon GREEK SMALL LETTER EPSILON */ + { 0x07e6, 0x03b6 }, /* Greek_zeta GREEK SMALL LETTER ZETA */ + { 0x07e7, 0x03b7 }, /* Greek_eta GREEK SMALL LETTER ETA */ + { 0x07e8, 0x03b8 }, /* Greek_theta GREEK SMALL LETTER THETA */ + { 0x07e9, 0x03b9 }, /* Greek_iota GREEK SMALL LETTER IOTA */ + { 0x07ea, 0x03ba }, /* Greek_kappa GREEK SMALL LETTER KAPPA */ + { 0x07eb, 0x03bb }, /* Greek_lambda GREEK SMALL LETTER LAMDA */ + { 0x07ec, 0x03bc }, /* Greek_mu GREEK SMALL LETTER MU */ + { 0x07ed, 0x03bd }, /* Greek_nu GREEK SMALL LETTER NU */ + { 0x07ee, 0x03be }, /* Greek_xi GREEK SMALL LETTER XI */ + { 0x07ef, 0x03bf }, /* Greek_omicron GREEK SMALL LETTER OMICRON */ + { 0x07f0, 0x03c0 }, /* Greek_pi GREEK SMALL LETTER PI */ + { 0x07f1, 0x03c1 }, /* Greek_rho GREEK SMALL LETTER RHO */ + { 0x07f2, 0x03c3 }, /* Greek_sigma GREEK SMALL LETTER SIGMA */ + { 0x07f3, 0x03c2 }, /* Greek_finalsmallsigma GREEK SMALL LETTER FINAL SIGMA */ + { 0x07f4, 0x03c4 }, /* Greek_tau GREEK SMALL LETTER TAU */ + { 0x07f5, 0x03c5 }, /* Greek_upsilon GREEK SMALL LETTER UPSILON */ + { 0x07f6, 0x03c6 }, /* Greek_phi GREEK SMALL LETTER PHI */ + { 0x07f7, 0x03c7 }, /* Greek_chi GREEK SMALL LETTER CHI */ + { 0x07f8, 0x03c8 }, /* Greek_psi GREEK SMALL LETTER PSI */ + { 0x07f9, 0x03c9 }, /* Greek_omega GREEK SMALL LETTER OMEGA */ + { 0x08a1, 0x23b7 }, /* leftradical ??? */ + { 0x08a2, 0x250c }, /* topleftradical BOX DRAWINGS LIGHT DOWN AND RIGHT */ + { 0x08a3, 0x2500 }, /* horizconnector BOX DRAWINGS LIGHT HORIZONTAL */ + { 0x08a4, 0x2320 }, /* topintegral TOP HALF INTEGRAL */ + { 0x08a5, 0x2321 }, /* botintegral BOTTOM HALF INTEGRAL */ + { 0x08a6, 0x2502 }, /* vertconnector BOX DRAWINGS LIGHT VERTICAL */ + { 0x08a7, 0x23a1 }, /* topleftsqbracket ??? */ + { 0x08a8, 0x23a3 }, /* botleftsqbracket ??? */ + { 0x08a9, 0x23a4 }, /* toprightsqbracket ??? */ + { 0x08aa, 0x23a6 }, /* botrightsqbracket ??? */ + { 0x08ab, 0x239b }, /* topleftparens ??? */ + { 0x08ac, 0x239d }, /* botleftparens ??? */ + { 0x08ad, 0x239e }, /* toprightparens ??? */ + { 0x08ae, 0x23a0 }, /* botrightparens ??? */ + { 0x08af, 0x23a8 }, /* leftmiddlecurlybrace ??? */ + { 0x08b0, 0x23ac }, /* rightmiddlecurlybrace ??? */ + /* 0x08b1 topleftsummation ??? */ + /* 0x08b2 botleftsummation ??? */ + /* 0x08b3 topvertsummationconnector ??? */ + /* 0x08b4 botvertsummationconnector ??? */ + /* 0x08b5 toprightsummation ??? */ + /* 0x08b6 botrightsummation ??? */ + /* 0x08b7 rightmiddlesummation ??? */ + { 0x08bc, 0x2264 }, /* lessthanequal LESS-THAN OR EQUAL TO */ + { 0x08bd, 0x2260 }, /* notequal NOT EQUAL TO */ + { 0x08be, 0x2265 }, /* greaterthanequal GREATER-THAN OR EQUAL TO */ + { 0x08bf, 0x222b }, /* integral INTEGRAL */ + { 0x08c0, 0x2234 }, /* therefore THEREFORE */ + { 0x08c1, 0x221d }, /* variation PROPORTIONAL TO */ + { 0x08c2, 0x221e }, /* infinity INFINITY */ + { 0x08c5, 0x2207 }, /* nabla NABLA */ + { 0x08c8, 0x223c }, /* approximate TILDE OPERATOR */ + { 0x08c9, 0x2243 }, /* similarequal ASYMPTOTICALLY EQUAL TO */ + { 0x08cd, 0x21d4 }, /* ifonlyif LEFT RIGHT DOUBLE ARROW */ + { 0x08ce, 0x21d2 }, /* implies RIGHTWARDS DOUBLE ARROW */ + { 0x08cf, 0x2261 }, /* identical IDENTICAL TO */ + { 0x08d6, 0x221a }, /* radical SQUARE ROOT */ + { 0x08da, 0x2282 }, /* includedin SUBSET OF */ + { 0x08db, 0x2283 }, /* includes SUPERSET OF */ + { 0x08dc, 0x2229 }, /* intersection INTERSECTION */ + { 0x08dd, 0x222a }, /* union UNION */ + { 0x08de, 0x2227 }, /* logicaland LOGICAL AND */ + { 0x08df, 0x2228 }, /* logicalor LOGICAL OR */ + { 0x08ef, 0x2202 }, /* partialderivative PARTIAL DIFFERENTIAL */ + { 0x08f6, 0x0192 }, /* function LATIN SMALL LETTER F WITH HOOK */ + { 0x08fb, 0x2190 }, /* leftarrow LEFTWARDS ARROW */ + { 0x08fc, 0x2191 }, /* uparrow UPWARDS ARROW */ + { 0x08fd, 0x2192 }, /* rightarrow RIGHTWARDS ARROW */ + { 0x08fe, 0x2193 }, /* downarrow DOWNWARDS ARROW */ + /* 0x09df blank ??? */ + { 0x09e0, 0x25c6 }, /* soliddiamond BLACK DIAMOND */ + { 0x09e1, 0x2592 }, /* checkerboard MEDIUM SHADE */ + { 0x09e2, 0x2409 }, /* ht SYMBOL FOR HORIZONTAL TABULATION */ + { 0x09e3, 0x240c }, /* ff SYMBOL FOR FORM FEED */ + { 0x09e4, 0x240d }, /* cr SYMBOL FOR CARRIAGE RETURN */ + { 0x09e5, 0x240a }, /* lf SYMBOL FOR LINE FEED */ + { 0x09e8, 0x2424 }, /* nl SYMBOL FOR NEWLINE */ + { 0x09e9, 0x240b }, /* vt SYMBOL FOR VERTICAL TABULATION */ + { 0x09ea, 0x2518 }, /* lowrightcorner BOX DRAWINGS LIGHT UP AND LEFT */ + { 0x09eb, 0x2510 }, /* uprightcorner BOX DRAWINGS LIGHT DOWN AND LEFT */ + { 0x09ec, 0x250c }, /* upleftcorner BOX DRAWINGS LIGHT DOWN AND RIGHT */ + { 0x09ed, 0x2514 }, /* lowleftcorner BOX DRAWINGS LIGHT UP AND RIGHT */ + { 0x09ee, 0x253c }, /* crossinglines BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */ + { 0x09ef, 0x23ba }, /* horizlinescan1 HORIZONTAL SCAN LINE-1 (Unicode 3.2 draft) */ + { 0x09f0, 0x23bb }, /* horizlinescan3 HORIZONTAL SCAN LINE-3 (Unicode 3.2 draft) */ + { 0x09f1, 0x2500 }, /* horizlinescan5 BOX DRAWINGS LIGHT HORIZONTAL */ + { 0x09f2, 0x23bc }, /* horizlinescan7 HORIZONTAL SCAN LINE-7 (Unicode 3.2 draft) */ + { 0x09f3, 0x23bd }, /* horizlinescan9 HORIZONTAL SCAN LINE-9 (Unicode 3.2 draft) */ + { 0x09f4, 0x251c }, /* leftt BOX DRAWINGS LIGHT VERTICAL AND RIGHT */ + { 0x09f5, 0x2524 }, /* rightt BOX DRAWINGS LIGHT VERTICAL AND LEFT */ + { 0x09f6, 0x2534 }, /* bott BOX DRAWINGS LIGHT UP AND HORIZONTAL */ + { 0x09f7, 0x252c }, /* topt BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */ + { 0x09f8, 0x2502 }, /* vertbar BOX DRAWINGS LIGHT VERTICAL */ + { 0x0aa1, 0x2003 }, /* emspace EM SPACE */ + { 0x0aa2, 0x2002 }, /* enspace EN SPACE */ + { 0x0aa3, 0x2004 }, /* em3space THREE-PER-EM SPACE */ + { 0x0aa4, 0x2005 }, /* em4space FOUR-PER-EM SPACE */ + { 0x0aa5, 0x2007 }, /* digitspace FIGURE SPACE */ + { 0x0aa6, 0x2008 }, /* punctspace PUNCTUATION SPACE */ + { 0x0aa7, 0x2009 }, /* thinspace THIN SPACE */ + { 0x0aa8, 0x200a }, /* hairspace HAIR SPACE */ + { 0x0aa9, 0x2014 }, /* emdash EM DASH */ + { 0x0aaa, 0x2013 }, /* endash EN DASH */ + /* 0x0aac signifblank ??? */ + { 0x0aae, 0x2026 }, /* ellipsis HORIZONTAL ELLIPSIS */ + { 0x0aaf, 0x2025 }, /* doubbaselinedot TWO DOT LEADER */ + { 0x0ab0, 0x2153 }, /* onethird VULGAR FRACTION ONE THIRD */ + { 0x0ab1, 0x2154 }, /* twothirds VULGAR FRACTION TWO THIRDS */ + { 0x0ab2, 0x2155 }, /* onefifth VULGAR FRACTION ONE FIFTH */ + { 0x0ab3, 0x2156 }, /* twofifths VULGAR FRACTION TWO FIFTHS */ + { 0x0ab4, 0x2157 }, /* threefifths VULGAR FRACTION THREE FIFTHS */ + { 0x0ab5, 0x2158 }, /* fourfifths VULGAR FRACTION FOUR FIFTHS */ + { 0x0ab6, 0x2159 }, /* onesixth VULGAR FRACTION ONE SIXTH */ + { 0x0ab7, 0x215a }, /* fivesixths VULGAR FRACTION FIVE SIXTHS */ + { 0x0ab8, 0x2105 }, /* careof CARE OF */ + { 0x0abb, 0x2012 }, /* figdash FIGURE DASH */ + { 0x0abc, 0x2329 }, /* leftanglebracket LEFT-POINTING ANGLE BRACKET */ + /* 0x0abd decimalpoint ??? */ + { 0x0abe, 0x232a }, /* rightanglebracket RIGHT-POINTING ANGLE BRACKET */ + /* 0x0abf marker ??? */ + { 0x0ac3, 0x215b }, /* oneeighth VULGAR FRACTION ONE EIGHTH */ + { 0x0ac4, 0x215c }, /* threeeighths VULGAR FRACTION THREE EIGHTHS */ + { 0x0ac5, 0x215d }, /* fiveeighths VULGAR FRACTION FIVE EIGHTHS */ + { 0x0ac6, 0x215e }, /* seveneighths VULGAR FRACTION SEVEN EIGHTHS */ + { 0x0ac9, 0x2122 }, /* trademark TRADE MARK SIGN */ + { 0x0aca, 0x2613 }, /* signaturemark SALTIRE */ + /* 0x0acb trademarkincircle ??? */ + { 0x0acc, 0x25c1 }, /* leftopentriangle WHITE LEFT-POINTING TRIANGLE */ + { 0x0acd, 0x25b7 }, /* rightopentriangle WHITE RIGHT-POINTING TRIANGLE */ + { 0x0ace, 0x25cb }, /* emopencircle WHITE CIRCLE */ + { 0x0acf, 0x25af }, /* emopenrectangle WHITE VERTICAL RECTANGLE */ + { 0x0ad0, 0x2018 }, /* leftsinglequotemark LEFT SINGLE QUOTATION MARK */ + { 0x0ad1, 0x2019 }, /* rightsinglequotemark RIGHT SINGLE QUOTATION MARK */ + { 0x0ad2, 0x201c }, /* leftdoublequotemark LEFT DOUBLE QUOTATION MARK */ + { 0x0ad3, 0x201d }, /* rightdoublequotemark RIGHT DOUBLE QUOTATION MARK */ + { 0x0ad4, 0x211e }, /* prescription PRESCRIPTION TAKE */ + { 0x0ad6, 0x2032 }, /* minutes PRIME */ + { 0x0ad7, 0x2033 }, /* seconds DOUBLE PRIME */ + { 0x0ad9, 0x271d }, /* latincross LATIN CROSS */ + /* 0x0ada hexagram ??? */ + { 0x0adb, 0x25ac }, /* filledrectbullet BLACK RECTANGLE */ + { 0x0adc, 0x25c0 }, /* filledlefttribullet BLACK LEFT-POINTING TRIANGLE */ + { 0x0add, 0x25b6 }, /* filledrighttribullet BLACK RIGHT-POINTING TRIANGLE */ + { 0x0ade, 0x25cf }, /* emfilledcircle BLACK CIRCLE */ + { 0x0adf, 0x25ae }, /* emfilledrect BLACK VERTICAL RECTANGLE */ + { 0x0ae0, 0x25e6 }, /* enopencircbullet WHITE BULLET */ + { 0x0ae1, 0x25ab }, /* enopensquarebullet WHITE SMALL SQUARE */ + { 0x0ae2, 0x25ad }, /* openrectbullet WHITE RECTANGLE */ + { 0x0ae3, 0x25b3 }, /* opentribulletup WHITE UP-POINTING TRIANGLE */ + { 0x0ae4, 0x25bd }, /* opentribulletdown WHITE DOWN-POINTING TRIANGLE */ + { 0x0ae5, 0x2606 }, /* openstar WHITE STAR */ + { 0x0ae6, 0x2022 }, /* enfilledcircbullet BULLET */ + { 0x0ae7, 0x25aa }, /* enfilledsqbullet BLACK SMALL SQUARE */ + { 0x0ae8, 0x25b2 }, /* filledtribulletup BLACK UP-POINTING TRIANGLE */ + { 0x0ae9, 0x25bc }, /* filledtribulletdown BLACK DOWN-POINTING TRIANGLE */ + { 0x0aea, 0x261c }, /* leftpointer WHITE LEFT POINTING INDEX */ + { 0x0aeb, 0x261e }, /* rightpointer WHITE RIGHT POINTING INDEX */ + { 0x0aec, 0x2663 }, /* club BLACK CLUB SUIT */ + { 0x0aed, 0x2666 }, /* diamond BLACK DIAMOND SUIT */ + { 0x0aee, 0x2665 }, /* heart BLACK HEART SUIT */ + { 0x0af0, 0x2720 }, /* maltesecross MALTESE CROSS */ + { 0x0af1, 0x2020 }, /* dagger DAGGER */ + { 0x0af2, 0x2021 }, /* doubledagger DOUBLE DAGGER */ + { 0x0af3, 0x2713 }, /* checkmark CHECK MARK */ + { 0x0af4, 0x2717 }, /* ballotcross BALLOT X */ + { 0x0af5, 0x266f }, /* musicalsharp MUSIC SHARP SIGN */ + { 0x0af6, 0x266d }, /* musicalflat MUSIC FLAT SIGN */ + { 0x0af7, 0x2642 }, /* malesymbol MALE SIGN */ + { 0x0af8, 0x2640 }, /* femalesymbol FEMALE SIGN */ + { 0x0af9, 0x260e }, /* telephone BLACK TELEPHONE */ + { 0x0afa, 0x2315 }, /* telephonerecorder TELEPHONE RECORDER */ + { 0x0afb, 0x2117 }, /* phonographcopyright SOUND RECORDING COPYRIGHT */ + { 0x0afc, 0x2038 }, /* caret CARET */ + { 0x0afd, 0x201a }, /* singlelowquotemark SINGLE LOW-9 QUOTATION MARK */ + { 0x0afe, 0x201e }, /* doublelowquotemark DOUBLE LOW-9 QUOTATION MARK */ + /* 0x0aff cursor ??? */ + { 0x0ba3, 0x003c }, /* leftcaret LESS-THAN SIGN */ + { 0x0ba6, 0x003e }, /* rightcaret GREATER-THAN SIGN */ + { 0x0ba8, 0x2228 }, /* downcaret LOGICAL OR */ + { 0x0ba9, 0x2227 }, /* upcaret LOGICAL AND */ + { 0x0bc0, 0x00af }, /* overbar MACRON */ + { 0x0bc2, 0x22a5 }, /* downtack UP TACK */ + { 0x0bc3, 0x2229 }, /* upshoe INTERSECTION */ + { 0x0bc4, 0x230a }, /* downstile LEFT FLOOR */ + { 0x0bc6, 0x005f }, /* underbar LOW LINE */ + { 0x0bca, 0x2218 }, /* jot RING OPERATOR */ + { 0x0bcc, 0x2395 }, /* quad APL FUNCTIONAL SYMBOL QUAD */ + { 0x0bce, 0x22a4 }, /* uptack DOWN TACK */ + { 0x0bcf, 0x25cb }, /* circle WHITE CIRCLE */ + { 0x0bd3, 0x2308 }, /* upstile LEFT CEILING */ + { 0x0bd6, 0x222a }, /* downshoe UNION */ + { 0x0bd8, 0x2283 }, /* rightshoe SUPERSET OF */ + { 0x0bda, 0x2282 }, /* leftshoe SUBSET OF */ + { 0x0bdc, 0x22a2 }, /* lefttack RIGHT TACK */ + { 0x0bfc, 0x22a3 }, /* righttack LEFT TACK */ + { 0x0cdf, 0x2017 }, /* hebrew_doublelowline DOUBLE LOW LINE */ + { 0x0ce0, 0x05d0 }, /* hebrew_aleph HEBREW LETTER ALEF */ + { 0x0ce1, 0x05d1 }, /* hebrew_bet HEBREW LETTER BET */ + { 0x0ce2, 0x05d2 }, /* hebrew_gimel HEBREW LETTER GIMEL */ + { 0x0ce3, 0x05d3 }, /* hebrew_dalet HEBREW LETTER DALET */ + { 0x0ce4, 0x05d4 }, /* hebrew_he HEBREW LETTER HE */ + { 0x0ce5, 0x05d5 }, /* hebrew_waw HEBREW LETTER VAV */ + { 0x0ce6, 0x05d6 }, /* hebrew_zain HEBREW LETTER ZAYIN */ + { 0x0ce7, 0x05d7 }, /* hebrew_chet HEBREW LETTER HET */ + { 0x0ce8, 0x05d8 }, /* hebrew_tet HEBREW LETTER TET */ + { 0x0ce9, 0x05d9 }, /* hebrew_yod HEBREW LETTER YOD */ + { 0x0cea, 0x05da }, /* hebrew_finalkaph HEBREW LETTER FINAL KAF */ + { 0x0ceb, 0x05db }, /* hebrew_kaph HEBREW LETTER KAF */ + { 0x0cec, 0x05dc }, /* hebrew_lamed HEBREW LETTER LAMED */ + { 0x0ced, 0x05dd }, /* hebrew_finalmem HEBREW LETTER FINAL MEM */ + { 0x0cee, 0x05de }, /* hebrew_mem HEBREW LETTER MEM */ + { 0x0cef, 0x05df }, /* hebrew_finalnun HEBREW LETTER FINAL NUN */ + { 0x0cf0, 0x05e0 }, /* hebrew_nun HEBREW LETTER NUN */ + { 0x0cf1, 0x05e1 }, /* hebrew_samech HEBREW LETTER SAMEKH */ + { 0x0cf2, 0x05e2 }, /* hebrew_ayin HEBREW LETTER AYIN */ + { 0x0cf3, 0x05e3 }, /* hebrew_finalpe HEBREW LETTER FINAL PE */ + { 0x0cf4, 0x05e4 }, /* hebrew_pe HEBREW LETTER PE */ + { 0x0cf5, 0x05e5 }, /* hebrew_finalzade HEBREW LETTER FINAL TSADI */ + { 0x0cf6, 0x05e6 }, /* hebrew_zade HEBREW LETTER TSADI */ + { 0x0cf7, 0x05e7 }, /* hebrew_qoph HEBREW LETTER QOF */ + { 0x0cf8, 0x05e8 }, /* hebrew_resh HEBREW LETTER RESH */ + { 0x0cf9, 0x05e9 }, /* hebrew_shin HEBREW LETTER SHIN */ + { 0x0cfa, 0x05ea }, /* hebrew_taw HEBREW LETTER TAV */ + { 0x0da1, 0x0e01 }, /* Thai_kokai THAI CHARACTER KO KAI */ + { 0x0da2, 0x0e02 }, /* Thai_khokhai THAI CHARACTER KHO KHAI */ + { 0x0da3, 0x0e03 }, /* Thai_khokhuat THAI CHARACTER KHO KHUAT */ + { 0x0da4, 0x0e04 }, /* Thai_khokhwai THAI CHARACTER KHO KHWAI */ + { 0x0da5, 0x0e05 }, /* Thai_khokhon THAI CHARACTER KHO KHON */ + { 0x0da6, 0x0e06 }, /* Thai_khorakhang THAI CHARACTER KHO RAKHANG */ + { 0x0da7, 0x0e07 }, /* Thai_ngongu THAI CHARACTER NGO NGU */ + { 0x0da8, 0x0e08 }, /* Thai_chochan THAI CHARACTER CHO CHAN */ + { 0x0da9, 0x0e09 }, /* Thai_choching THAI CHARACTER CHO CHING */ + { 0x0daa, 0x0e0a }, /* Thai_chochang THAI CHARACTER CHO CHANG */ + { 0x0dab, 0x0e0b }, /* Thai_soso THAI CHARACTER SO SO */ + { 0x0dac, 0x0e0c }, /* Thai_chochoe THAI CHARACTER CHO CHOE */ + { 0x0dad, 0x0e0d }, /* Thai_yoying THAI CHARACTER YO YING */ + { 0x0dae, 0x0e0e }, /* Thai_dochada THAI CHARACTER DO CHADA */ + { 0x0daf, 0x0e0f }, /* Thai_topatak THAI CHARACTER TO PATAK */ + { 0x0db0, 0x0e10 }, /* Thai_thothan THAI CHARACTER THO THAN */ + { 0x0db1, 0x0e11 }, /* Thai_thonangmontho THAI CHARACTER THO NANGMONTHO */ + { 0x0db2, 0x0e12 }, /* Thai_thophuthao THAI CHARACTER THO PHUTHAO */ + { 0x0db3, 0x0e13 }, /* Thai_nonen THAI CHARACTER NO NEN */ + { 0x0db4, 0x0e14 }, /* Thai_dodek THAI CHARACTER DO DEK */ + { 0x0db5, 0x0e15 }, /* Thai_totao THAI CHARACTER TO TAO */ + { 0x0db6, 0x0e16 }, /* Thai_thothung THAI CHARACTER THO THUNG */ + { 0x0db7, 0x0e17 }, /* Thai_thothahan THAI CHARACTER THO THAHAN */ + { 0x0db8, 0x0e18 }, /* Thai_thothong THAI CHARACTER THO THONG */ + { 0x0db9, 0x0e19 }, /* Thai_nonu THAI CHARACTER NO NU */ + { 0x0dba, 0x0e1a }, /* Thai_bobaimai THAI CHARACTER BO BAIMAI */ + { 0x0dbb, 0x0e1b }, /* Thai_popla THAI CHARACTER PO PLA */ + { 0x0dbc, 0x0e1c }, /* Thai_phophung THAI CHARACTER PHO PHUNG */ + { 0x0dbd, 0x0e1d }, /* Thai_fofa THAI CHARACTER FO FA */ + { 0x0dbe, 0x0e1e }, /* Thai_phophan THAI CHARACTER PHO PHAN */ + { 0x0dbf, 0x0e1f }, /* Thai_fofan THAI CHARACTER FO FAN */ + { 0x0dc0, 0x0e20 }, /* Thai_phosamphao THAI CHARACTER PHO SAMPHAO */ + { 0x0dc1, 0x0e21 }, /* Thai_moma THAI CHARACTER MO MA */ + { 0x0dc2, 0x0e22 }, /* Thai_yoyak THAI CHARACTER YO YAK */ + { 0x0dc3, 0x0e23 }, /* Thai_rorua THAI CHARACTER RO RUA */ + { 0x0dc4, 0x0e24 }, /* Thai_ru THAI CHARACTER RU */ + { 0x0dc5, 0x0e25 }, /* Thai_loling THAI CHARACTER LO LING */ + { 0x0dc6, 0x0e26 }, /* Thai_lu THAI CHARACTER LU */ + { 0x0dc7, 0x0e27 }, /* Thai_wowaen THAI CHARACTER WO WAEN */ + { 0x0dc8, 0x0e28 }, /* Thai_sosala THAI CHARACTER SO SALA */ + { 0x0dc9, 0x0e29 }, /* Thai_sorusi THAI CHARACTER SO RUSI */ + { 0x0dca, 0x0e2a }, /* Thai_sosua THAI CHARACTER SO SUA */ + { 0x0dcb, 0x0e2b }, /* Thai_hohip THAI CHARACTER HO HIP */ + { 0x0dcc, 0x0e2c }, /* Thai_lochula THAI CHARACTER LO CHULA */ + { 0x0dcd, 0x0e2d }, /* Thai_oang THAI CHARACTER O ANG */ + { 0x0dce, 0x0e2e }, /* Thai_honokhuk THAI CHARACTER HO NOKHUK */ + { 0x0dcf, 0x0e2f }, /* Thai_paiyannoi THAI CHARACTER PAIYANNOI */ + { 0x0dd0, 0x0e30 }, /* Thai_saraa THAI CHARACTER SARA A */ + { 0x0dd1, 0x0e31 }, /* Thai_maihanakat THAI CHARACTER MAI HAN-AKAT */ + { 0x0dd2, 0x0e32 }, /* Thai_saraaa THAI CHARACTER SARA AA */ + { 0x0dd3, 0x0e33 }, /* Thai_saraam THAI CHARACTER SARA AM */ + { 0x0dd4, 0x0e34 }, /* Thai_sarai THAI CHARACTER SARA I */ + { 0x0dd5, 0x0e35 }, /* Thai_saraii THAI CHARACTER SARA II */ + { 0x0dd6, 0x0e36 }, /* Thai_saraue THAI CHARACTER SARA UE */ + { 0x0dd7, 0x0e37 }, /* Thai_sarauee THAI CHARACTER SARA UEE */ + { 0x0dd8, 0x0e38 }, /* Thai_sarau THAI CHARACTER SARA U */ + { 0x0dd9, 0x0e39 }, /* Thai_sarauu THAI CHARACTER SARA UU */ + { 0x0dda, 0x0e3a }, /* Thai_phinthu THAI CHARACTER PHINTHU */ + /* 0x0dde Thai_maihanakat_maitho ??? */ + { 0x0ddf, 0x0e3f }, /* Thai_baht THAI CURRENCY SYMBOL BAHT */ + { 0x0de0, 0x0e40 }, /* Thai_sarae THAI CHARACTER SARA E */ + { 0x0de1, 0x0e41 }, /* Thai_saraae THAI CHARACTER SARA AE */ + { 0x0de2, 0x0e42 }, /* Thai_sarao THAI CHARACTER SARA O */ + { 0x0de3, 0x0e43 }, /* Thai_saraaimaimuan THAI CHARACTER SARA AI MAIMUAN */ + { 0x0de4, 0x0e44 }, /* Thai_saraaimaimalai THAI CHARACTER SARA AI MAIMALAI */ + { 0x0de5, 0x0e45 }, /* Thai_lakkhangyao THAI CHARACTER LAKKHANGYAO */ + { 0x0de6, 0x0e46 }, /* Thai_maiyamok THAI CHARACTER MAIYAMOK */ + { 0x0de7, 0x0e47 }, /* Thai_maitaikhu THAI CHARACTER MAITAIKHU */ + { 0x0de8, 0x0e48 }, /* Thai_maiek THAI CHARACTER MAI EK */ + { 0x0de9, 0x0e49 }, /* Thai_maitho THAI CHARACTER MAI THO */ + { 0x0dea, 0x0e4a }, /* Thai_maitri THAI CHARACTER MAI TRI */ + { 0x0deb, 0x0e4b }, /* Thai_maichattawa THAI CHARACTER MAI CHATTAWA */ + { 0x0dec, 0x0e4c }, /* Thai_thanthakhat THAI CHARACTER THANTHAKHAT */ + { 0x0ded, 0x0e4d }, /* Thai_nikhahit THAI CHARACTER NIKHAHIT */ + { 0x0df0, 0x0e50 }, /* Thai_leksun THAI DIGIT ZERO */ + { 0x0df1, 0x0e51 }, /* Thai_leknung THAI DIGIT ONE */ + { 0x0df2, 0x0e52 }, /* Thai_leksong THAI DIGIT TWO */ + { 0x0df3, 0x0e53 }, /* Thai_leksam THAI DIGIT THREE */ + { 0x0df4, 0x0e54 }, /* Thai_leksi THAI DIGIT FOUR */ + { 0x0df5, 0x0e55 }, /* Thai_lekha THAI DIGIT FIVE */ + { 0x0df6, 0x0e56 }, /* Thai_lekhok THAI DIGIT SIX */ + { 0x0df7, 0x0e57 }, /* Thai_lekchet THAI DIGIT SEVEN */ + { 0x0df8, 0x0e58 }, /* Thai_lekpaet THAI DIGIT EIGHT */ + { 0x0df9, 0x0e59 }, /* Thai_lekkao THAI DIGIT NINE */ + { 0x0ea1, 0x3131 }, /* Hangul_Kiyeog HANGUL LETTER KIYEOK */ + { 0x0ea2, 0x3132 }, /* Hangul_SsangKiyeog HANGUL LETTER SSANGKIYEOK */ + { 0x0ea3, 0x3133 }, /* Hangul_KiyeogSios HANGUL LETTER KIYEOK-SIOS */ + { 0x0ea4, 0x3134 }, /* Hangul_Nieun HANGUL LETTER NIEUN */ + { 0x0ea5, 0x3135 }, /* Hangul_NieunJieuj HANGUL LETTER NIEUN-CIEUC */ + { 0x0ea6, 0x3136 }, /* Hangul_NieunHieuh HANGUL LETTER NIEUN-HIEUH */ + { 0x0ea7, 0x3137 }, /* Hangul_Dikeud HANGUL LETTER TIKEUT */ + { 0x0ea8, 0x3138 }, /* Hangul_SsangDikeud HANGUL LETTER SSANGTIKEUT */ + { 0x0ea9, 0x3139 }, /* Hangul_Rieul HANGUL LETTER RIEUL */ + { 0x0eaa, 0x313a }, /* Hangul_RieulKiyeog HANGUL LETTER RIEUL-KIYEOK */ + { 0x0eab, 0x313b }, /* Hangul_RieulMieum HANGUL LETTER RIEUL-MIEUM */ + { 0x0eac, 0x313c }, /* Hangul_RieulPieub HANGUL LETTER RIEUL-PIEUP */ + { 0x0ead, 0x313d }, /* Hangul_RieulSios HANGUL LETTER RIEUL-SIOS */ + { 0x0eae, 0x313e }, /* Hangul_RieulTieut HANGUL LETTER RIEUL-THIEUTH */ + { 0x0eaf, 0x313f }, /* Hangul_RieulPhieuf HANGUL LETTER RIEUL-PHIEUPH */ + { 0x0eb0, 0x3140 }, /* Hangul_RieulHieuh HANGUL LETTER RIEUL-HIEUH */ + { 0x0eb1, 0x3141 }, /* Hangul_Mieum HANGUL LETTER MIEUM */ + { 0x0eb2, 0x3142 }, /* Hangul_Pieub HANGUL LETTER PIEUP */ + { 0x0eb3, 0x3143 }, /* Hangul_SsangPieub HANGUL LETTER SSANGPIEUP */ + { 0x0eb4, 0x3144 }, /* Hangul_PieubSios HANGUL LETTER PIEUP-SIOS */ + { 0x0eb5, 0x3145 }, /* Hangul_Sios HANGUL LETTER SIOS */ + { 0x0eb6, 0x3146 }, /* Hangul_SsangSios HANGUL LETTER SSANGSIOS */ + { 0x0eb7, 0x3147 }, /* Hangul_Ieung HANGUL LETTER IEUNG */ + { 0x0eb8, 0x3148 }, /* Hangul_Jieuj HANGUL LETTER CIEUC */ + { 0x0eb9, 0x3149 }, /* Hangul_SsangJieuj HANGUL LETTER SSANGCIEUC */ + { 0x0eba, 0x314a }, /* Hangul_Cieuc HANGUL LETTER CHIEUCH */ + { 0x0ebb, 0x314b }, /* Hangul_Khieuq HANGUL LETTER KHIEUKH */ + { 0x0ebc, 0x314c }, /* Hangul_Tieut HANGUL LETTER THIEUTH */ + { 0x0ebd, 0x314d }, /* Hangul_Phieuf HANGUL LETTER PHIEUPH */ + { 0x0ebe, 0x314e }, /* Hangul_Hieuh HANGUL LETTER HIEUH */ + { 0x0ebf, 0x314f }, /* Hangul_A HANGUL LETTER A */ + { 0x0ec0, 0x3150 }, /* Hangul_AE HANGUL LETTER AE */ + { 0x0ec1, 0x3151 }, /* Hangul_YA HANGUL LETTER YA */ + { 0x0ec2, 0x3152 }, /* Hangul_YAE HANGUL LETTER YAE */ + { 0x0ec3, 0x3153 }, /* Hangul_EO HANGUL LETTER EO */ + { 0x0ec4, 0x3154 }, /* Hangul_E HANGUL LETTER E */ + { 0x0ec5, 0x3155 }, /* Hangul_YEO HANGUL LETTER YEO */ + { 0x0ec6, 0x3156 }, /* Hangul_YE HANGUL LETTER YE */ + { 0x0ec7, 0x3157 }, /* Hangul_O HANGUL LETTER O */ + { 0x0ec8, 0x3158 }, /* Hangul_WA HANGUL LETTER WA */ + { 0x0ec9, 0x3159 }, /* Hangul_WAE HANGUL LETTER WAE */ + { 0x0eca, 0x315a }, /* Hangul_OE HANGUL LETTER OE */ + { 0x0ecb, 0x315b }, /* Hangul_YO HANGUL LETTER YO */ + { 0x0ecc, 0x315c }, /* Hangul_U HANGUL LETTER U */ + { 0x0ecd, 0x315d }, /* Hangul_WEO HANGUL LETTER WEO */ + { 0x0ece, 0x315e }, /* Hangul_WE HANGUL LETTER WE */ + { 0x0ecf, 0x315f }, /* Hangul_WI HANGUL LETTER WI */ + { 0x0ed0, 0x3160 }, /* Hangul_YU HANGUL LETTER YU */ + { 0x0ed1, 0x3161 }, /* Hangul_EU HANGUL LETTER EU */ + { 0x0ed2, 0x3162 }, /* Hangul_YI HANGUL LETTER YI */ + { 0x0ed3, 0x3163 }, /* Hangul_I HANGUL LETTER I */ + { 0x0ed4, 0x11a8 }, /* Hangul_J_Kiyeog HANGUL JONGSEONG KIYEOK */ + { 0x0ed5, 0x11a9 }, /* Hangul_J_SsangKiyeog HANGUL JONGSEONG SSANGKIYEOK */ + { 0x0ed6, 0x11aa }, /* Hangul_J_KiyeogSios HANGUL JONGSEONG KIYEOK-SIOS */ + { 0x0ed7, 0x11ab }, /* Hangul_J_Nieun HANGUL JONGSEONG NIEUN */ + { 0x0ed8, 0x11ac }, /* Hangul_J_NieunJieuj HANGUL JONGSEONG NIEUN-CIEUC */ + { 0x0ed9, 0x11ad }, /* Hangul_J_NieunHieuh HANGUL JONGSEONG NIEUN-HIEUH */ + { 0x0eda, 0x11ae }, /* Hangul_J_Dikeud HANGUL JONGSEONG TIKEUT */ + { 0x0edb, 0x11af }, /* Hangul_J_Rieul HANGUL JONGSEONG RIEUL */ + { 0x0edc, 0x11b0 }, /* Hangul_J_RieulKiyeog HANGUL JONGSEONG RIEUL-KIYEOK */ + { 0x0edd, 0x11b1 }, /* Hangul_J_RieulMieum HANGUL JONGSEONG RIEUL-MIEUM */ + { 0x0ede, 0x11b2 }, /* Hangul_J_RieulPieub HANGUL JONGSEONG RIEUL-PIEUP */ + { 0x0edf, 0x11b3 }, /* Hangul_J_RieulSios HANGUL JONGSEONG RIEUL-SIOS */ + { 0x0ee0, 0x11b4 }, /* Hangul_J_RieulTieut HANGUL JONGSEONG RIEUL-THIEUTH */ + { 0x0ee1, 0x11b5 }, /* Hangul_J_RieulPhieuf HANGUL JONGSEONG RIEUL-PHIEUPH */ + { 0x0ee2, 0x11b6 }, /* Hangul_J_RieulHieuh HANGUL JONGSEONG RIEUL-HIEUH */ + { 0x0ee3, 0x11b7 }, /* Hangul_J_Mieum HANGUL JONGSEONG MIEUM */ + { 0x0ee4, 0x11b8 }, /* Hangul_J_Pieub HANGUL JONGSEONG PIEUP */ + { 0x0ee5, 0x11b9 }, /* Hangul_J_PieubSios HANGUL JONGSEONG PIEUP-SIOS */ + { 0x0ee6, 0x11ba }, /* Hangul_J_Sios HANGUL JONGSEONG SIOS */ + { 0x0ee7, 0x11bb }, /* Hangul_J_SsangSios HANGUL JONGSEONG SSANGSIOS */ + { 0x0ee8, 0x11bc }, /* Hangul_J_Ieung HANGUL JONGSEONG IEUNG */ + { 0x0ee9, 0x11bd }, /* Hangul_J_Jieuj HANGUL JONGSEONG CIEUC */ + { 0x0eea, 0x11be }, /* Hangul_J_Cieuc HANGUL JONGSEONG CHIEUCH */ + { 0x0eeb, 0x11bf }, /* Hangul_J_Khieuq HANGUL JONGSEONG KHIEUKH */ + { 0x0eec, 0x11c0 }, /* Hangul_J_Tieut HANGUL JONGSEONG THIEUTH */ + { 0x0eed, 0x11c1 }, /* Hangul_J_Phieuf HANGUL JONGSEONG PHIEUPH */ + { 0x0eee, 0x11c2 }, /* Hangul_J_Hieuh HANGUL JONGSEONG HIEUH */ + { 0x0eef, 0x316d }, /* Hangul_RieulYeorinHieuh HANGUL LETTER RIEUL-YEORINHIEUH */ + { 0x0ef0, 0x3171 }, /* Hangul_SunkyeongeumMieum HANGUL LETTER KAPYEOUNMIEUM */ + { 0x0ef1, 0x3178 }, /* Hangul_SunkyeongeumPieub HANGUL LETTER KAPYEOUNPIEUP */ + { 0x0ef2, 0x317f }, /* Hangul_PanSios HANGUL LETTER PANSIOS */ + { 0x0ef3, 0x3181 }, /* Hangul_KkogjiDalrinIeung HANGUL LETTER YESIEUNG */ + { 0x0ef4, 0x3184 }, /* Hangul_SunkyeongeumPhieuf HANGUL LETTER KAPYEOUNPHIEUPH */ + { 0x0ef5, 0x3186 }, /* Hangul_YeorinHieuh HANGUL LETTER YEORINHIEUH */ + { 0x0ef6, 0x318d }, /* Hangul_AraeA HANGUL LETTER ARAEA */ + { 0x0ef7, 0x318e }, /* Hangul_AraeAE HANGUL LETTER ARAEAE */ + { 0x0ef8, 0x11eb }, /* Hangul_J_PanSios HANGUL JONGSEONG PANSIOS */ + { 0x0ef9, 0x11f0 }, /* Hangul_J_KkogjiDalrinIeung HANGUL JONGSEONG YESIEUNG */ + { 0x0efa, 0x11f9 }, /* Hangul_J_YeorinHieuh HANGUL JONGSEONG YEORINHIEUH */ + { 0x0eff, 0x20a9 }, /* Korean_Won WON SIGN */ + { 0x13a4, 0x20ac }, /* Euro EURO SIGN */ + { 0x13bc, 0x0152 }, /* OE LATIN CAPITAL LIGATURE OE */ + { 0x13bd, 0x0153 }, /* oe LATIN SMALL LIGATURE OE */ + { 0x13be, 0x0178 }, /* Ydiaeresis LATIN CAPITAL LETTER Y WITH DIAERESIS */ + { 0x20ac, 0x20ac }, /* EuroSign EURO SIGN */ + }); + + private static Map toMap(int[][] keys) { + Map keyMap = new HashMap(); + for (int[] km: keys) { + keyMap.put(km[1], km[0]); + } + return keyMap; + } + + public static int unicode2keysym(int ch) { + if (ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255) + return ch; + Integer converted = keyMap.get(ch); + return converted != null ? + converted : + // No variants has been found for the unicode symbol then pass as unicode + // with the special flag. + // Note this method is valid only for 0x100-0x10ffff unicode value range. + ch | 0x01000000; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/utils/LazyLoaded.java b/plugins/vnc/src/main/java/com/glavsoft/utils/LazyLoaded.java new file mode 100644 index 0000000..a09c06b --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/utils/LazyLoaded.java @@ -0,0 +1,63 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.utils; + +/** + * @author dime at tightvnc.com + */ +public class LazyLoaded { + private boolean isLoaded; + private T lazyObj; + private Loader loader; + + private LazyLoaded() { + } + + public T get() { + if (isLoaded) { + return lazyObj; + } else { + try { + lazyObj = loader.load(); + isLoaded = true; + } catch (Throwable ignore) { + return null; + } + return lazyObj; + } + } + + public LazyLoaded(Loader loader) { + this.loader = loader; + } + + public interface Loader { + /** + * Loads the lazy loaded object + * + * @return object loaded + */ + T load() throws Throwable; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/utils/Strings.java b/plugins/vnc/src/main/java/com/glavsoft/utils/Strings.java new file mode 100644 index 0000000..11a5111 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/utils/Strings.java @@ -0,0 +1,62 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.utils; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +public class Strings { + public static String toString(byte[] byteArray) { + StringBuilder sb = new StringBuilder("["); + boolean notFirst = false; + for (byte b : byteArray) { + if (notFirst) { + sb.append(", "); + } else { + notFirst = true; + } + sb.append(b); + } + return sb.append("]").toString(); + } + + public static boolean isTrimmedEmpty(String s) { + return null == s || (s.trim().length() == 0); + } + + public static byte[] getBytesWithCharset(String string, Charset charset) { + byte[] result; + try { + result = string.getBytes(charset); + } catch (NoSuchMethodError error) { + try { + result = string.getBytes(charset.name()); + } catch (UnsupportedEncodingException e) { + result = null; + } + } + return result; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/utils/ViewerControlApi.java b/plugins/vnc/src/main/java/com/glavsoft/utils/ViewerControlApi.java new file mode 100644 index 0000000..4f1256c --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/utils/ViewerControlApi.java @@ -0,0 +1,86 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.utils; + +import com.glavsoft.rfb.client.ClientMessageType; +import com.glavsoft.rfb.client.ClientToServerMessage; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.transport.BaudrateMeter; + +/** + * @author dime at tightvnc.com + */ +public class ViewerControlApi { + private final Protocol protocol; + private BaudrateMeter baudrateMeter; + + public ViewerControlApi(Protocol protocol, BaudrateMeter baudrateMeter) { + this.protocol = protocol; + this.baudrateMeter = baudrateMeter; + protocol.setBaudrateMeter(baudrateMeter); + } + + public void sendMessage(ClientToServerMessage message) { + protocol.sendMessage(message); + } + + public void sendKeepAlive() { + protocol.sendSupportedEncodingsMessage(protocol.getSettings()); + } + + public void setCompressionLevelTo(int compressionLevel) { + final ProtocolSettings settings = protocol.getSettings(); + settings.setCompressionLevel(compressionLevel); + settings.fireListeners(); + } + + public void setJpegQualityTo(int jpegQuality) { + final ProtocolSettings settings = protocol.getSettings(); + settings.setJpegQuality(jpegQuality); + settings.fireListeners(); + } + + public void setViewOnly(boolean isViewOnly) { + final ProtocolSettings settings = protocol.getSettings(); + settings.setViewOnly(isViewOnly); + settings.fireListeners(); + } + + + public int getBaudrate() { + return baudrateMeter.kBPS(); + } + + /** + * Check whether remote server is supported for given client-to-server message + * + * @param type client-to-server message type to check for + * @return true when supported + */ + public boolean isSupported(ClientMessageType type) { + return protocol.isSupported(type); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/cli/Parser.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/cli/Parser.java new file mode 100644 index 0000000..37fe507 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/cli/Parser.java @@ -0,0 +1,119 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.cli; + +import com.glavsoft.utils.Strings; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Command line interface parameters parser + */ +public class Parser { + private final Map options = new LinkedHashMap(); + private final List plainOptions = new ArrayList(); + private boolean isSetPlainOptions = false; + + public void addOption(String opName, String defaultValue, String desc) { + Option op = new Option(opName, defaultValue, desc); + options.put(opName.toLowerCase(), op); + } + + public void parse(String [] args) { + for (String p : args) { + if (p.startsWith("-")) { + int skipMinuses = p.startsWith("--") ? 2 : 1; + String[] params = p.split("=", 2); + Option op = options.get(params[0].toLowerCase().substring(skipMinuses)); + if (op != null) { + op.isSet = true; + if (params.length > 1 && ! Strings.isTrimmedEmpty(params[1])) { + op.value = params[1]; + } + } + } else if ( ! p.startsWith("-")) { + isSetPlainOptions = true; + plainOptions.add(p); + } + } + } + + public String getValueFor(String param) { + Option op = options.get(param.toLowerCase()); + return op != null ? op.value : null; + } + + public boolean isSet(String param) { + Option op = options.get(param.toLowerCase()); + return op != null && op.isSet; + } + + public boolean isSetPlainOptions() { + return isSetPlainOptions; + } + + public String getPlainOptionAt(int index) { + return plainOptions.get(index); + } + + public int getPlainOptionsNumber() { + return plainOptions.size(); + } + + /** + * Command line interface option + */ + private static class Option { + String opName; + String desc; + String value; + boolean isSet = false; + Option(String opName, String defaultValue, String desc) { + this.opName = opName; + this.desc = desc; + this.value = defaultValue; + } + } + + public String optionsUsage() { + StringBuilder sb = new StringBuilder(); + int maxNameLength = 0; + for (Option op : options.values()) { + if (op.desc.isEmpty()) continue; + maxNameLength = Math.max(maxNameLength, op.opName.length()); + } + for (Option op : options.values()) { + if (op.desc.isEmpty()) continue; + sb.append(" -").append(op.opName); + for (int i=0; i registeredViews; + private final Map registeredModels; + static private Logger logger = Logger.getLogger(Presenter.class.getName()); + private Throwable savedInvocationTargetException; + + public Presenter() { + registeredViews = new HashMap(); + registeredModels = new HashMap(); + } + + /** + * Register View at the Presenter + * + * @param name - name (id) of View + * @param view - the View + */ + public void addView(String name, View view) { + registeredViews.put(name, view); + } + + public Set> removeAllViews() { + final Set> save = registeredViews.entrySet(); + registeredViews.clear(); + return save; + } + /** + * Register Model at the Presenter + * + * @param name - name (id) of Model + * @param model - the Model + */ + public void addModel(String name, Model model) { + registeredModels.put(name, model); + } + + /** + * Iterate over Models and pass available model properties of each model into Views + * @see #populateFrom(String) for more details + */ + protected void populate() { + savedInvocationTargetException = null; + for (Map.Entry entry : registeredModels.entrySet()) { + String modelName = entry.getKey(); + Model model = entry.getValue(); + populateFrom(modelName, model); + } + } + + /** + * Iterate over model's getters, gets model property (name, value and its value type), then tries to set view's property + * with the same name and value type. Skip property getting or setting on any access errors + * + * @param modelName name of Model + */ + public void populateFrom(String modelName) { + Model model = registeredModels.get(modelName); + if (model != null) { + populateFrom(modelName, model); + } else { + logger.finer("Cannot find model: " + modelName); + } + } + + private void populateFrom(String modelName, Model model) { + Method methods[] = model.getClass().getDeclaredMethods(); + for (Method m : methods) { + if (m.getName().startsWith("get") && m.getParameterTypes().length == 0) { + String propertyName = m.getName().substring(3); + try { + final Object property = m.invoke(model); + logger.finest("Load: " + modelName + ".get" + propertyName + "() # => " + property + + " type: " + m.getReturnType()); + setViewProperty(propertyName, property, m.getReturnType()); // TODO this can set savedInvocationTargetEx, so what to do whith it? + } catch (IllegalAccessException ignore) { + // nop + } catch (InvocationTargetException e) { + savedInvocationTargetException = e.getCause(); // TODO may be skip it? + break; + } + } + } + } + + protected boolean isModelRegisteredByName(String modelName) { + return registeredModels.containsKey(modelName); + } + + protected Model getModel(String modelName) { + return registeredModels.get(modelName); + } + + protected void show() { + for (View v : registeredViews.values()) { + v.showView(); + } + } + + + protected void save() { + savedInvocationTargetException = null; + for (Map.Entry entry : registeredModels.entrySet()) { + String modelName = entry.getKey(); + Model model = entry.getValue(); + Method methods[] = model.getClass().getDeclaredMethods(); + for (Method m : methods) { + if (m.getName().startsWith("set")) { + String propertyName = m.getName().substring(3); + try { + final Object viewProperty = getViewProperty(propertyName); + m.invoke(model, viewProperty); + logger.finest("Save: " + modelName + ".set" + propertyName + "( " + viewProperty + " )"); + } catch (IllegalAccessException ignore) { + // nop + } catch (InvocationTargetException e) { + savedInvocationTargetException = e.getCause(); + break; + } catch (PropertyNotFoundException e) { + // nop + } + } + } + } + } + + public Object getViewPropertyOrNull(String propertyName) { + try { + return getViewProperty(propertyName); + } catch (PropertyNotFoundException e) { + return null; + } + } + + public Object getViewProperty(String propertyName) throws PropertyNotFoundException { + savedInvocationTargetException = null; + logger.finest("get" + propertyName + "()"); + for (Map.Entry entry : registeredViews.entrySet()) { + String viewName = entry.getKey(); + View view = entry.getValue(); + try { + Method getter = view.getClass().getMethod("get" + propertyName, new Class[0]); + final Object res = getter.invoke(view); + logger.finest("----from view: " + viewName + ".get" + propertyName + "() # +> " + res); + return res; + // oops, only first getter will be found TODO? + } catch (NoSuchMethodException ignore) { + // nop + } catch (InvocationTargetException e) { + savedInvocationTargetException = e.getCause(); + break; + } catch (IllegalAccessException ignore) { + // nop + } + } + throw new PropertyNotFoundException(propertyName); + } + + public Object getModelProperty(String propertyName) { + savedInvocationTargetException = null; + logger.finest("get" + propertyName + "()"); + for (String modelName : registeredModels.keySet()) { + Model model = registeredModels.get(modelName); + try { + Method getter = model.getClass().getMethod("get" + propertyName, new Class[0]); + final Object res = getter.invoke(model); + logger.finest("----from model: " + modelName + ".get" + propertyName + "() # +> " + res); + return res; + // oops, only first getter will be found TODO? + } catch (NoSuchMethodException ignore) { + // nop + } catch (InvocationTargetException e) { + savedInvocationTargetException = e.getCause(); + break; + } catch (IllegalAccessException ignore) { + // nop + } + } +// savedInvocationTargetException = new PropertyNotFoundException(propertyName); + return null; + } + + public void setViewProperty(String propertyName, Object newValue) { + setViewProperty(propertyName, newValue, newValue.getClass()); + } + + public void setViewProperty(String propertyName, Object newValue, Class valueType) { + savedInvocationTargetException = null; + logger.finest("set" + propertyName + "( " + newValue + " ) type: " + valueType); + for (Map.Entry entry : registeredViews.entrySet()) { + String viewName = entry.getKey(); + View view = entry.getValue(); + try { + Method setter = view.getClass().getMethod("set" + propertyName, valueType); + setter.invoke(view, newValue); + logger.finest("----to view: " + viewName + ".set" + propertyName + "( " + newValue + " )"); + } catch (NoSuchMethodException ignore) { + // nop + } catch (InvocationTargetException e) { + e.getCause().printStackTrace(); + savedInvocationTargetException = e.getCause(); + break; + } catch (IllegalAccessException ignore) { + // nop + } + } + } + + protected void throwPossiblyHappenedException() throws Throwable { + if (savedInvocationTargetException != null) { + Throwable tmp = savedInvocationTargetException; + savedInvocationTargetException = null; + throw tmp; + } + } + + protected View getView(String name) { + return registeredViews.get(name); + } + + public void setModelProperty(String propertyName, Object newValue) { + setModelProperty(propertyName, newValue, newValue.getClass()); + } + + public void setModelProperty(String propertyName, Object newValue, Class valueType) { + savedInvocationTargetException = null; + logger.finest("set" + propertyName + "( " + newValue + " )"); + for (Map.Entry entry : registeredModels.entrySet()) { + String modelName = entry.getKey(); + Model model = entry.getValue(); + try { + Method method = model.getClass().getMethod("set" + propertyName, valueType); + method.invoke(model, newValue); + logger.finest("----for model: " + modelName); + } catch (NoSuchMethodException ignore) { + // nop + } catch (InvocationTargetException e) { + savedInvocationTargetException = e.getCause(); + break; + } catch (IllegalAccessException ignore) { + // nop + } + } + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/PropertyNotFoundException.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/PropertyNotFoundException.java new file mode 100644 index 0000000..648e71e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/PropertyNotFoundException.java @@ -0,0 +1,35 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.mvp; + +import com.glavsoft.exceptions.CommonException; + +/** +* @author dime at glavsoft.com +*/ +public class PropertyNotFoundException extends CommonException { + public PropertyNotFoundException(String message) { + super(message); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/View.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/View.java new file mode 100644 index 0000000..80faf5a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/mvp/View.java @@ -0,0 +1,45 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.mvp; + +/** + * View layer for Model-View-Presenter architecture + * + * The View is a visible and user interacted object. The Presenter layer of architecture determines when to show + * the View and provide data flow between views and models. + * The View must have a set of getters and setters for data properties it represents. + * + * @author dime at tightvnc.com + */ +public interface View { + /** + * Make the view visible + */ + void showView(); + + /** + * Close the view + */ + void closeView(); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/ConnectionParams.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/ConnectionParams.java new file mode 100644 index 0000000..5b2c796 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/ConnectionParams.java @@ -0,0 +1,284 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.settings; + +import com.glavsoft.utils.Strings; +import com.glavsoft.viewer.mvp.Model; + +/** + * Object that represents parameters needed for establishing network connection to remote host. + * This is used to pass a number of parameters into connection establishing module and + * to provide a Connection History interface feature. + * + * @author dime at tightvnc.com + */ +public class ConnectionParams implements Model { + public static final int DEFAULT_SSH_PORT = 22; + private static final int DEFAULT_RFB_PORT = 5900; + + /** + * A name of remote host. + * Rather symbolic (dns) name or ip address + * Ex. remote.host.mydomain.com or localhost or 192.168.0.2 etc. + */ + public String hostName; + /** + * A port number of remote host. + * Default is 5900 + */ + private int portNumber; + + public String sshUserName; + public String sshHostName; + private int sshPortNumber; + + private boolean useSsh; + + public ConnectionParams(String hostName, int portNumber, boolean useSsh, String sshHostName, int sshPortNumber, String sshUserName) { + this.hostName = hostName; + this.portNumber = portNumber; + this.sshUserName = sshUserName; + this.sshHostName = sshHostName; + this.sshPortNumber = sshPortNumber; + this.useSsh = useSsh; + } + + public ConnectionParams(ConnectionParams cp) { + this.hostName = cp.hostName != null? cp.hostName: ""; + this.portNumber = cp.portNumber; + this.sshUserName = cp.sshUserName; + this.sshHostName = cp.sshHostName; + this.sshPortNumber = cp.sshPortNumber; + this.useSsh = cp.useSsh; + } + + public ConnectionParams() { + hostName = ""; + sshUserName = ""; + sshHostName = ""; + } + + public ConnectionParams(String hostName, String portNumber) { + this.hostName = hostName; + try { + setPortNumber(portNumber); + } catch (WrongParameterException ignore) { + // use default + this.portNumber = 0; + } + } + + /** + * Check host name empty + * @return true if host name is empty + */ + public boolean isHostNameEmpty() { + return Strings.isTrimmedEmpty(hostName); + } + + /** + * Parse port number from string specified. + * Thrown WrongParameterException on error. + * + * @param port string representation of port number + * @throws WrongParameterException when parsing error occurs or port number is out of range + * @return portNubmer parsed + */ + private int parsePortNumber(String port) throws WrongParameterException { + int portNumber; + if (null == port) return 0; + try { + portNumber = Integer.parseInt(port); + } catch (NumberFormatException e) { + portNumber = 0; + if ( ! Strings.isTrimmedEmpty(port)) { + throw new WrongParameterException("Wrong port number: " + port + "\nMust be in 0..65535"); + } + } + if (portNumber > 65535 || portNumber < 0) { + throw new WrongParameterException("Port number is out of range: " + port + "\nMust be in 0..65535"); + } + return portNumber; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + public String getHostName() { + return this.hostName; + } + + /** + * Parse port number from string specified. + * Thrown WrongParameterException on error. + * Set portNumber property when port on success. + * throws WrongParameterException when parsing error occurs or port number is out of range + * + * @param port string representation of port number + */ + public void setPortNumber(String port) throws WrongParameterException { + portNumber = this.parsePortNumber(port); + } + + public void setPortNumber(int port) { + this.portNumber = port; + } + + public int getPortNumber() { + return 0 == portNumber ? DEFAULT_RFB_PORT : portNumber; + } + + public void setSshPortNumber(String port) throws WrongParameterException { + try { + sshPortNumber = this.parsePortNumber(port); + } catch (WrongParameterException e) { + throw new WrongParameterException("SSH port number error. " + e.getMessage()); + } + } + + public void setSshPortNumber(int port) { + this.sshPortNumber = port; + } + + public int getSshPortNumber() { + return 0 == sshPortNumber ? DEFAULT_SSH_PORT: sshPortNumber; + } + + /** + * Set flag to use SSH port forwarding when connect to host + * @param useSsh + */ + public void setUseSsh(boolean useSsh) { + this.useSsh = useSsh; + } + + /** + * Check to use SSH port forwarding when connect to host + * @return true if user wants to use SSH + */ + public boolean useSsh() { + return useSsh && ! Strings.isTrimmedEmpty(sshHostName); + } + + public boolean getUseSsh() { + return this.useSsh(); + } + + public String getSshUserName() { + return this.sshUserName; + } + + public void setSshUserName(String sshUserName) { + this.sshUserName = sshUserName; + } + + public String getSshHostName() { + return this.sshHostName; + } + + public void setSshHostName(String sshHostName) { + this.sshHostName = sshHostName; + } + + /** + * Copy and complete only field that are empty (null, zerro or empty string) in `this' object from the other one + * + * @param other ConnectionParams object to copy fields from + */ + public void completeEmptyFieldsFrom(ConnectionParams other) { + if (null == other) return; + if (Strings.isTrimmedEmpty(hostName) && ! Strings.isTrimmedEmpty(other.hostName)) { + hostName = other.hostName; + } + if ( 0 == portNumber && other.portNumber != 0) { + portNumber = other.portNumber; + } + if (Strings.isTrimmedEmpty(sshUserName) && ! Strings.isTrimmedEmpty(other.sshUserName)) { + sshUserName = other.sshUserName; + } + if (Strings.isTrimmedEmpty(sshHostName) && ! Strings.isTrimmedEmpty(other.sshHostName)) { + sshHostName = other.sshHostName; + } + if ( 0 == sshPortNumber && other.sshPortNumber != 0) { + sshPortNumber = other.sshPortNumber; + } + useSsh |= other.useSsh; + } + + @Override + public String toString() { + return hostName != null ? hostName : ""; +// return (hostName != null ? hostName : "") + ":" + portNumber + " " + useSsh + " " + sshUserName + "@" + sshHostName + ":" + sshPortNumber; + } + + /** + * For logging purpose + * + * @return string representation of object + */ + public String toPrint() { + return "ConnectionParams{" + + "hostName='" + hostName + '\'' + + ", portNumber=" + portNumber + + ", sshUserName='" + sshUserName + '\'' + + ", sshHostName='" + sshHostName + '\'' + + ", sshPortNumber=" + sshPortNumber + + ", useSsh=" + useSsh + + '}'; + } + + @Override + public boolean equals(Object obj) { + if (null == obj || ! (obj instanceof ConnectionParams)) return false; + if (this == obj) return true; + ConnectionParams o = (ConnectionParams) obj; + return isEqualsNullable(hostName, o.hostName) && getPortNumber() == o.getPortNumber() && + useSsh == o.useSsh && isEqualsNullable(sshHostName, o.sshHostName) && + getSshPortNumber() == o.getSshPortNumber() && isEqualsNullable(sshUserName, o.sshUserName); + } + + private boolean isEqualsNullable(String one, String another) { + return (null == one? "" : one).equals(null == another? "" : another); + } + + @Override + public int hashCode() { + long hash = (hostName != null? hostName.hashCode() : 0) + + portNumber * 17 + + (useSsh ? 781 : 693) + + (sshHostName != null? sshHostName.hashCode() : 0) * 23 + + (sshUserName != null? sshUserName.hashCode() : 0) * 37 + + sshPortNumber * 41; + return (int)hash; + } + + public void clearFields() { + hostName = ""; + portNumber = 0; + useSsh = false; + sshHostName = null; + sshUserName = null; + sshPortNumber = 0; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/LocalMouseCursorShape.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/LocalMouseCursorShape.java new file mode 100644 index 0000000..4df3a35 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/LocalMouseCursorShape.java @@ -0,0 +1,44 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.settings; + +/** + * @author dime at tightvnc.com + */ +public enum LocalMouseCursorShape { + DOT("dot"), + SMALL_DOT("smalldot"), + SYSTEM_DEFAULT("default"), + NO_CURSOR("nocursor"); + + private String cursorName; + + LocalMouseCursorShape(String name) { + this.cursorName = name; + } + + public String getCursorName() { + return cursorName; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettings.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettings.java new file mode 100644 index 0000000..825e791 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettings.java @@ -0,0 +1,189 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.settings; + +import com.glavsoft.core.SettingsChangedEvent; +import com.glavsoft.rfb.IChangeSettingsListener; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author dime at tightvnc.com + */ +public class UiSettings { + + public static final int MIN_SCALE_PERCENT = 10; + public static final int MAX_SCALE_PERCENT = 500; + private static final int SCALE_PERCENT_ZOOMING_STEP = 10; + + @SuppressWarnings("PointlessBitwiseExpression") + public static final int CHANGED_SCALE_FACTOR = 1 << 0; + public static final int CHANGED_MOUSE_CURSOR_SHAPE = 1 << 1; + public static final int CHANGED_FULL_SCREEN = 1 << 2; + + private final List listeners = new CopyOnWriteArrayList(); + private int changedSettingsMask = 0; + + private final UiSettingsData uiSettingsData; + public boolean showControls = true; + public boolean showConnectionDialog = false; + + public UiSettings() { + uiSettingsData = new UiSettingsData(); + changedSettingsMask = 0; + } + + public UiSettings(UiSettings uiSettings) { + uiSettingsData = new UiSettingsData( + uiSettings.getScalePercent(), uiSettings.getMouseCursorShape(), uiSettings.isFullScreen()); + this.changedSettingsMask = uiSettings.changedSettingsMask; + } + + public double getScaleFactor() { + return uiSettingsData.getScalePercent() / 100.; + } + + public void setScalePercent(double scalePercent) { + if (this.uiSettingsData.setScalePercent(scalePercent)) { + changedSettingsMask |= CHANGED_SCALE_FACTOR; + } + } + + public void addListener(IChangeSettingsListener listener) { + listeners.add(listener); + } + + void fireListeners() { + if (null == listeners) return; + final SettingsChangedEvent event = new SettingsChangedEvent(new UiSettings(this)); + changedSettingsMask = 0; + for (IChangeSettingsListener listener : listeners) { + listener.settingsChanged(event); + } + } + + public void zoomOut() { + double oldScaleFactor = uiSettingsData.getScalePercent(); + double scaleFactor = (int)(this.uiSettingsData.getScalePercent() / SCALE_PERCENT_ZOOMING_STEP) * SCALE_PERCENT_ZOOMING_STEP; + if (scaleFactor == oldScaleFactor) { + scaleFactor -= SCALE_PERCENT_ZOOMING_STEP; + } + if (scaleFactor < MIN_SCALE_PERCENT) { + scaleFactor = MIN_SCALE_PERCENT; + } + setScalePercent(scaleFactor); + fireListeners(); + } + + public void zoomIn() { + double scaleFactor = (int)(this.uiSettingsData.getScalePercent() / SCALE_PERCENT_ZOOMING_STEP) * SCALE_PERCENT_ZOOMING_STEP + SCALE_PERCENT_ZOOMING_STEP; + if (scaleFactor > MAX_SCALE_PERCENT) { + scaleFactor = MAX_SCALE_PERCENT; + } + setScalePercent(scaleFactor); + fireListeners(); + } + + public void zoomAsIs() { + setScalePercent(100); + fireListeners(); + } + + public void zoomToFit(int containerWidth, int containerHeight, int fbWidth, int fbHeight) { + int scalePromille = Math.min(1000 * containerWidth / fbWidth, + 1000 * containerHeight / fbHeight); + while (fbWidth * scalePromille / 1000. > containerWidth || + fbHeight * scalePromille / 1000. > containerHeight) { + scalePromille -= 1; + } + setScalePercent(scalePromille / 10.); + fireListeners(); + } + + public boolean isChangedMouseCursorShape() { + return (changedSettingsMask & CHANGED_MOUSE_CURSOR_SHAPE) == CHANGED_MOUSE_CURSOR_SHAPE; + } + + public static boolean isUiSettingsChangedFired(SettingsChangedEvent event) { + return event.getSource() instanceof UiSettings; + } + + public double getScalePercent() { + return uiSettingsData.getScalePercent(); + } + + public String getScalePercentFormatted() { + NumberFormat numberFormat = new DecimalFormat("###.#"); + return numberFormat.format(uiSettingsData.getScalePercent()); + } + + public LocalMouseCursorShape getMouseCursorShape() { + return uiSettingsData.getMouseCursorShape(); + } + + public void setMouseCursorShape(LocalMouseCursorShape mouseCursorShape) { + if (this.uiSettingsData.setMouseCursorShape(mouseCursorShape)) { + changedSettingsMask |= CHANGED_MOUSE_CURSOR_SHAPE; + fireListeners(); + } + } + + public void copyDataFrom(UiSettingsData other) { + copyDataFrom(other, 0); + } + public void copyDataFrom(UiSettingsData other, int mask) { + if (null == other) return; + if ((mask & CHANGED_SCALE_FACTOR) == 0) uiSettingsData.setScalePercent(other.getScalePercent()); + if ((mask & CHANGED_MOUSE_CURSOR_SHAPE) == 0) uiSettingsData.setMouseCursorShape(other.getMouseCursorShape()); + if ((mask & CHANGED_FULL_SCREEN) == 0) uiSettingsData.setFullScreen(other.isFullScreen()); + } + + public void setFullScreen(boolean isFullScreen) { + if (uiSettingsData.setFullScreen(isFullScreen)) { + changedSettingsMask |= CHANGED_FULL_SCREEN; + fireListeners(); + } + } + + public boolean isFullScreen() { + return uiSettingsData.isFullScreen(); + } + + public UiSettingsData getData() { + return uiSettingsData; + } + + @Override + public String toString() { + return "UiSettings{" + + "scalePercent=" + uiSettingsData.getScalePercent() + + ", fullScreen=" + uiSettingsData.isFullScreen() + + ", mouseCursorShape=" + uiSettingsData.getMouseCursorShape() + + '}'; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettingsData.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettingsData.java new file mode 100644 index 0000000..9056d3a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/UiSettingsData.java @@ -0,0 +1,99 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.settings; + +import java.io.Serializable; + +/** + * @author dime at tightvnc.com + */ +public class UiSettingsData implements Serializable { + private static final long serialVersionUID = 1L; + private double scalePercent; + private LocalMouseCursorShape mouseCursorShape; + private boolean fullScreen; + + + public UiSettingsData() { + scalePercent = 100; + mouseCursorShape = LocalMouseCursorShape.DOT; + fullScreen = false; + } + + public UiSettingsData(double scalePercent, LocalMouseCursorShape mouseCursorShape, boolean fullScreen) { + this.scalePercent = scalePercent; + this.mouseCursorShape = mouseCursorShape; + this.fullScreen = fullScreen; + } + + public UiSettingsData(UiSettingsData other) { + this(other.getScalePercent(), other.getMouseCursorShape(), other.isFullScreen()); + } + + public double getScalePercent() { + return scalePercent; + } + + public boolean setScalePercent(double scalePercent) { + if (this.scalePercent != scalePercent) { + this.scalePercent = scalePercent; + return true; + } + return false; + } + + + public LocalMouseCursorShape getMouseCursorShape() { + return mouseCursorShape; + } + + public boolean setMouseCursorShape(LocalMouseCursorShape mouseCursorShape) { + if (this.mouseCursorShape != mouseCursorShape && mouseCursorShape != null) { + this.mouseCursorShape = mouseCursorShape; + return true; + } + return false; + } + + public boolean isFullScreen() { + return fullScreen; + } + + public boolean setFullScreen(boolean fullScreen) { + if (this.fullScreen != fullScreen) { + this.fullScreen = fullScreen; + return true; + } + return false; + } + + @Override + public String toString() { + return "UiSettingsData{" + + "scalePercent=" + scalePercent + + ", mouseCursorShape=" + mouseCursorShape + + ", fullScreen=" + fullScreen + + '}'; + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/WrongParameterException.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/WrongParameterException.java new file mode 100644 index 0000000..a69ced4 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/settings/WrongParameterException.java @@ -0,0 +1,47 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.settings; + +import com.glavsoft.exceptions.CommonException; + +/** + * @author dime at tightvnc.com + */ +public class WrongParameterException extends CommonException { + + private String propertyName; + + public WrongParameterException(String message) { + super(message); + } + + public String getPropertyName() { + return propertyName; + } + + public WrongParameterException(String message, String propertyName) { + super(message); + this.propertyName = propertyName; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionException.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionException.java new file mode 100644 index 0000000..d27d62e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionException.java @@ -0,0 +1,36 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.exceptions.CommonException; + +/** + * @author dime at tightvnc.com + */ +public class CancelConnectionException extends CommonException { + + public CancelConnectionException(String message) { + super(message); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionQuietlyException.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionQuietlyException.java new file mode 100644 index 0000000..5126ad1 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/CancelConnectionQuietlyException.java @@ -0,0 +1,34 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +/** + * @author dime at tightvnc.com + */ +public class CancelConnectionQuietlyException extends CancelConnectionException { + + public CancelConnectionQuietlyException(String message) { + super(message); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ClipboardControllerImpl.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ClipboardControllerImpl.java new file mode 100644 index 0000000..9ab33f8 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ClipboardControllerImpl.java @@ -0,0 +1,152 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.core.SettingsChangedEvent; +import com.glavsoft.rfb.ClipboardController; +import com.glavsoft.rfb.client.ClientCutTextMessage; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.utils.Strings; + +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.nio.charset.Charset; + +public class ClipboardControllerImpl implements ClipboardController, Runnable { + private static final String STANDARD_CHARSET = "ISO-8859-1"; // aka Latin-1 + private static final long CLIPBOARD_UPDATE_CHECK_INTERVAL_MILS = 1000L; + private Clipboard clipboard; + private String clipboardText = null; + private volatile boolean isRunning; + private boolean isEnabled; + private final Protocol protocol; + private Charset charset; + + public ClipboardControllerImpl(Protocol protocol, String charsetName) { + this.protocol = protocol; + try { + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + updateSavedClipboardContent(); // prevent onstart clipboard content sending + } catch (SecurityException e) { /*nop*/ } + + if (Strings.isTrimmedEmpty(charsetName)) { + charset = Charset.defaultCharset(); + } else if ("standard".equalsIgnoreCase(charsetName)) { + charset = Charset.forName(STANDARD_CHARSET); + } else { + charset = Charset.isSupported(charsetName) ? Charset.forName(charsetName) : Charset.defaultCharset(); + } + // not supported UTF-charsets as they are multibytes. + // add others multibytes charsets on need + if (charset.name().startsWith("UTF")) { + charset = Charset.forName(STANDARD_CHARSET); + } + } + + @Override + public void updateSystemClipboard(byte[] bytes) { + if (clipboard != null) { + StringSelection stringSelection = new StringSelection(new String(bytes, charset)); + if (isEnabled) { + clipboard.setContents(stringSelection, null); + } + } + } + + /** + * Callback for clipboard changes listeners + * Retrieves text content from system clipboard which then available + * through getClipboardText(). + */ + private void updateSavedClipboardContent() { + if (clipboard != null && clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) { + try { + clipboardText = (String)clipboard.getData(DataFlavor.stringFlavor); + } catch (UnsupportedFlavorException e) { + // ignore + } catch (IOException e) { + // ignore + } + } else { + clipboardText = null; + } + } + + @Override + public String getClipboardText() { + return clipboardText; + } + + /** + * Get text clipboard contents when needed send to remote, or null vise versa + * + * @return clipboard string contents if it is changed from last method call + * or null when clipboard contains non text object or clipboard contents didn't changed + */ + @Override + public String getRenewedClipboardText() { + String old = clipboardText; + updateSavedClipboardContent(); + if (clipboardText != null && ! clipboardText.equals(old)) + return clipboardText; + return null; + } + + @Override + public void setEnabled(boolean enable) { + if (! enable) { + isRunning = false; + } + if (enable && ! isEnabled) { + new Thread(this).start(); + } + isEnabled = enable; + } + + @Override + public void run() { + isRunning = true; + while (isRunning) { + String clipboardText = getRenewedClipboardText(); + if (clipboardText != null) { + protocol.sendMessage(new ClientCutTextMessage(clipboardText, charset)); + } + try { + Thread.sleep(CLIPBOARD_UPDATE_CHECK_INTERVAL_MILS); + } catch (InterruptedException ignore) { } + } + } + + @Override + public void settingsChanged(SettingsChangedEvent e) { + ProtocolSettings settings = (ProtocolSettings) e.getSource(); + setEnabled(settings.isAllowClipboardTransfer()); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionErrorException.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionErrorException.java new file mode 100644 index 0000000..5dcecb9 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionErrorException.java @@ -0,0 +1,36 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.exceptions.CommonException; + +/** + * @author dime at tightvnc.com + */ +public class ConnectionErrorException extends CommonException { + + public ConnectionErrorException(String message) { + super(message); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionPresenter.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionPresenter.java new file mode 100644 index 0000000..33085a6 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ConnectionPresenter.java @@ -0,0 +1,370 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.utils.Strings; +import com.glavsoft.utils.ViewerControlApi; +import com.glavsoft.viewer.mvp.Presenter; +import com.glavsoft.viewer.mvp.PropertyNotFoundException; +import com.glavsoft.viewer.settings.ConnectionParams; +import com.glavsoft.viewer.settings.UiSettings; +import com.glavsoft.viewer.settings.WrongParameterException; +import com.glavsoft.viewer.swing.gui.ConnectionView; +import com.glavsoft.viewer.swing.gui.ConnectionsHistory; +import com.glavsoft.viewer.swing.ssh.SshConnectionManager; +import com.glavsoft.viewer.workers.AbstractConnectionWorkerFactory; +import com.glavsoft.viewer.workers.NetworkConnectionWorker; +import com.glavsoft.viewer.workers.RfbConnectionWorker; + +import java.net.Socket; +import java.util.logging.Logger; + +/** + * A Presenter (controller) that presents business logic for connection establishing interactions + * + * Before starting connection process you have to add View(s) and Model(s), and need to set @see ConnectionWorkerFactory + * + * @author dime at tightvnc.com + */ +public class ConnectionPresenter extends Presenter { + public static final String PROPERTY_HOST_NAME = "HostName"; + public static final String PROPERTY_RFB_PORT_NUMBER = "PortNumber"; + public static final String PROPERTY_USE_SSH = "UseSsh"; + private static final String PROPERTY_SSH_USER_NAME = "SshUserName"; + private static final String PROPERTY_SSH_HOST_NAME = "SshHostName"; + private static final String PROPERTY_SSH_PORT_NUMBER = "SshPortNumber"; + private static final String PROPERTY_STATUS_BAR_MESSAGE = "Message"; + private static final String PROPERTY_CONNECTION_IN_PROGRESS = "ConnectionInProgress"; + public static final String CONNECTION_PARAMS_MODEL = "ConnectionParamsModel"; + public static final String CONNECTIONS_HISTORY_MODEL = "ConnectionsHistoryModel"; + public static final String CONNECTION_VIEW = "ConnectionView"; + + private final boolean hasSshSupport; + private ConnectionsHistory connectionsHistory; + private ProtocolSettings rfbSettings; + private UiSettings uiSettings; + private final Logger logger; + private RfbConnectionWorker rfbConnectionWorker; + private AbstractConnectionWorkerFactory connectionWorkerFactory; + private NetworkConnectionWorker networkConnectionWorker; + private boolean needReconnection = true; + private ViewerControlApi viewerControlApi; + + public ConnectionPresenter() { + this.hasSshSupport = SshConnectionManager.checkForSshSupport(); + + logger = Logger.getLogger(getClass().getName()); + } + + public void startConnection(ProtocolSettings rfbSettings, UiSettings uiSettings) + throws IllegalStateException { + startConnection(rfbSettings, uiSettings, 0); + } + + public void startConnection(ProtocolSettings rfbSettings, UiSettings uiSettings, int paramSettingsMask) + throws IllegalStateException { + this.rfbSettings = rfbSettings; + this.uiSettings = uiSettings; + if (!isModelRegisteredByName(CONNECTION_PARAMS_MODEL)) { + throw new IllegalStateException("No Connection Params model added."); + } + connectionsHistory = new ConnectionsHistory(); + addModel(CONNECTIONS_HISTORY_MODEL, connectionsHistory); + syncModels(paramSettingsMask); + show(); + populate(); + if ( ! uiSettings.showConnectionDialog) { + connect(); + } + } + + public void setUseSsh(boolean useSsh) { + setModelProperty(PROPERTY_USE_SSH, useSsh, boolean.class); + } + + /** + * Initiate connection process from View + * ex. by press "Connect" button + * + * @param hostName name of host to connect + */ + public void submitConnection(String hostName) throws WrongParameterException { + if (Strings.isTrimmedEmpty(hostName)) { + throw new WrongParameterException("Host name is empty", PROPERTY_HOST_NAME); + } + setModelProperty(PROPERTY_HOST_NAME, hostName); + + final String rfbPort = (String) getViewPropertyOrNull(PROPERTY_RFB_PORT_NUMBER); + setModelProperty(PROPERTY_RFB_PORT_NUMBER, rfbPort); + try { + throwPossiblyHappenedException(); + } catch (Throwable e) { + throw new WrongParameterException("Wrong Port", PROPERTY_HOST_NAME); + } + setSshOptions(); + + saveHistory(); + populateFrom(CONNECTIONS_HISTORY_MODEL); + + connect(); + } + + /** + * Cause ConnectionHistory prop to be saved + */ + void saveHistory() { + final ConnectionParams cp = (ConnectionParams) getModel(CONNECTION_PARAMS_MODEL); + connectionsHistory.reorder(cp, rfbSettings, uiSettings); + connectionsHistory.save(); + } + + /** + * Prepares async network connection worker and starts to execute it + * Network connection worker tries to establish tcp connection with remote host + */ + public void connect() { + final ConnectionParams connectionParams = (ConnectionParams) getModel(CONNECTION_PARAMS_MODEL); + if (null == connectionWorkerFactory) { + throw new IllegalStateException("connectionWorkerFactory is not set"); + } + networkConnectionWorker = connectionWorkerFactory.createNetworkConnectionWorker(); + networkConnectionWorker.setConnectionParams(connectionParams); + networkConnectionWorker.setPresenter(this); + networkConnectionWorker.setHasSshSupport(hasSshSupport); + networkConnectionWorker.execute(); + } + + /** + * Callback for connection worker, invoked on failed connection attempt. + * Both for tcp network connection worker and for rfb connection worker. + */ + void connectionFailed() { + cancelConnection(); + reconnect(null); + } + + /** + * Callback for connection worker, invoked when connection is cancelled + */ + void connectionCancelled() { + cancelConnection(); + enableConnectionDialog(); + } + + private void enableConnectionDialog() { + setViewProperty(PROPERTY_CONNECTION_IN_PROGRESS, false, boolean.class); + } + + /** + * Callback for tcp network connection worker. + * Invoked on successful connection. + * Invoked in EDT + * + * @param workingSocket a socket binded with established connection + */ + void successfulNetworkConnection(Socket workingSocket) { // EDT + logger.info("Connected"); + showMessage("Connected"); + rfbConnectionWorker = connectionWorkerFactory.createRfbConnectionWorker(); + rfbConnectionWorker.setWorkingSocket(workingSocket); + rfbConnectionWorker.setRfbSettings(rfbSettings); + rfbConnectionWorker.setUiSettings(uiSettings); + rfbConnectionWorker.setConnectionString( + getModelProperty(PROPERTY_HOST_NAME) + ":" + getModelProperty(PROPERTY_RFB_PORT_NUMBER)); + rfbConnectionWorker.execute(); + viewerControlApi = rfbConnectionWorker.getViewerControlApi(); + } + + /** + * Callback for rfb connection worker + * Invoked on successful connection + */ + void successfulRfbConnection() { + enableConnectionDialog(); + getView(CONNECTION_VIEW).closeView(); + } + + /** + * Gracefully cancel active connection workers + */ + public void cancelConnection() { + logger.finer("Cancel connection"); + if (networkConnectionWorker != null) { + networkConnectionWorker.cancel(); + } + if (rfbConnectionWorker != null) { + rfbConnectionWorker.cancel(); + } + } + + /** + * Ask ConnectionView to show dialog + * + * @param errorMessage message to show + */ + void showConnectionErrorDialog(String errorMessage) { + final ConnectionView connectionView = (ConnectionView) getView(CONNECTION_VIEW); + if (connectionView != null) { + connectionView.showConnectionErrorDialog(errorMessage); + } + } + + /** + * Ask ConnectionView to show dialog whether to reconnect or close app + * + * @param errorTitle dialog title to show + * @param errorMessage message to show + */ + void showReconnectDialog(String errorTitle, String errorMessage) { + final ConnectionView connectionView = (ConnectionView) getView(CONNECTION_VIEW); + if (connectionView != null) { + connectionView.showReconnectDialog(errorTitle, errorMessage); + } + } + + private void setSshOptions() { + if (hasSshSupport) { + try { + final boolean useSsh = (Boolean) getViewProperty(PROPERTY_USE_SSH); + setModelProperty(PROPERTY_USE_SSH, useSsh, boolean.class); + } catch (PropertyNotFoundException e) { + //nop + } + setModelProperty(PROPERTY_SSH_USER_NAME, getViewPropertyOrNull(PROPERTY_SSH_USER_NAME), String.class); + setModelProperty(PROPERTY_SSH_HOST_NAME, getViewPropertyOrNull(PROPERTY_SSH_HOST_NAME), String.class); + setModelProperty(PROPERTY_SSH_PORT_NUMBER, getViewPropertyOrNull(PROPERTY_SSH_PORT_NUMBER)); + setViewProperty(PROPERTY_SSH_PORT_NUMBER, getModelProperty(PROPERTY_SSH_PORT_NUMBER)); + } + } + + private void syncModels(int paramSettingsMask) { + final ConnectionParams cp = (ConnectionParams) getModel(CONNECTION_PARAMS_MODEL); + final ConnectionParams mostSuitableConnection = connectionsHistory.getMostSuitableConnection(cp); + cp.completeEmptyFieldsFrom(mostSuitableConnection); + rfbSettings.copyDataFrom(connectionsHistory.getProtocolSettings(mostSuitableConnection), paramSettingsMask & 0xffff); + uiSettings.copyDataFrom(connectionsHistory.getUiSettingsData(mostSuitableConnection), (paramSettingsMask >> 16) & 0xffff); + if (!cp.isHostNameEmpty()) { + connectionsHistory.reorder(cp, rfbSettings, uiSettings); + } + +// protocolSettings.addListener(connectionsHistory); +// uiSettings.addListener(connectionsHistory); + } + + /** + * populate Model(s) props with data from param passed (network connection related props) + * populate View(s) props from model(s) + * get rfb and ui settings from History + * + * @param connectionParams connection parameters + */ + public void populateFromHistoryItem(ConnectionParams connectionParams) { + setModelProperty(PROPERTY_HOST_NAME, connectionParams.hostName, String.class); + setModelProperty(PROPERTY_RFB_PORT_NUMBER, connectionParams.getPortNumber(), int.class); + setModelProperty(PROPERTY_USE_SSH, connectionParams.useSsh(), boolean.class); + setModelProperty(PROPERTY_SSH_HOST_NAME, connectionParams.sshHostName, String.class); + setModelProperty(PROPERTY_SSH_PORT_NUMBER, connectionParams.getSshPortNumber(), int.class); + setModelProperty(PROPERTY_SSH_USER_NAME, connectionParams.sshUserName, String.class); + populateFrom(CONNECTION_PARAMS_MODEL); + rfbSettings.copyDataFrom(connectionsHistory.getProtocolSettings(connectionParams)); + uiSettings.copyDataFrom(connectionsHistory.getUiSettingsData(connectionParams)); + } + + /** + * Forget history records at all + */ + public void clearHistory() { + connectionsHistory.clear(); + connectionsHistory.reorder((ConnectionParams) getModel(CONNECTION_PARAMS_MODEL), rfbSettings, uiSettings); + populateFrom(CONNECTIONS_HISTORY_MODEL); + clearMessage(); + } + + /** + * Show status info about currently executed operation + * + * @param message status message + */ + void showMessage(String message) { + setViewProperty(PROPERTY_STATUS_BAR_MESSAGE, message); + } + + /** + * Show empty status bar + */ + void clearMessage() { + showMessage(""); + } + + /** + * Set connection worker factory + * + * @param connectionWorkerFactory factory + */ + public void setConnectionWorkerFactory(AbstractConnectionWorkerFactory connectionWorkerFactory) { + this.connectionWorkerFactory = connectionWorkerFactory; + } + + /** + * Reset presenter and try to start new connection establishing process + * + * @param predefinedPassword password + */ + void reconnect(String predefinedPassword) { + if (predefinedPassword != null && ! predefinedPassword.isEmpty()) + connectionWorkerFactory.setPredefinedPassword(predefinedPassword); + clearMessage(); + enableConnectionDialog(); + show(); + populate(); + if ( ! uiSettings.showConnectionDialog) { + connect(); + } + } + + void clearPredefinedPassword() { + connectionWorkerFactory.setPredefinedPassword(null); + } + + public UiSettings getUiSettings() { + return uiSettings; + } + + public ProtocolSettings getRfbSettings() { + return rfbSettings; + } + + boolean needReconnection() { + return needReconnection; + } + + public void setNeedReconnection(boolean need) { + needReconnection = need; + } + + public ViewerControlApi getViewerControlApi() { + return viewerControlApi; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyEventListener.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyEventListener.java new file mode 100644 index 0000000..2db8818 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyEventListener.java @@ -0,0 +1,268 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.rfb.client.KeyEventMessage; +import com.glavsoft.rfb.protocol.Protocol; + +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import static com.glavsoft.utils.Keymap.K_ALT_LEFT; +import static com.glavsoft.utils.Keymap.K_BACK_SPACE; +import static com.glavsoft.utils.Keymap.K_CTRL_LEFT; +import static com.glavsoft.utils.Keymap.K_DELETE; +import static com.glavsoft.utils.Keymap.K_DOWN; +import static com.glavsoft.utils.Keymap.K_END; +import static com.glavsoft.utils.Keymap.K_ENTER; +import static com.glavsoft.utils.Keymap.K_ESCAPE; +import static com.glavsoft.utils.Keymap.K_F1; +import static com.glavsoft.utils.Keymap.K_F10; +import static com.glavsoft.utils.Keymap.K_F11; +import static com.glavsoft.utils.Keymap.K_F12; +import static com.glavsoft.utils.Keymap.K_F2; +import static com.glavsoft.utils.Keymap.K_F3; +import static com.glavsoft.utils.Keymap.K_F4; +import static com.glavsoft.utils.Keymap.K_F5; +import static com.glavsoft.utils.Keymap.K_F6; +import static com.glavsoft.utils.Keymap.K_F7; +import static com.glavsoft.utils.Keymap.K_F8; +import static com.glavsoft.utils.Keymap.K_F9; +import static com.glavsoft.utils.Keymap.K_HOME; +import static com.glavsoft.utils.Keymap.K_HYPER_LEFT; +import static com.glavsoft.utils.Keymap.K_INSERT; +import static com.glavsoft.utils.Keymap.K_KP_0; +import static com.glavsoft.utils.Keymap.K_KP_1; +import static com.glavsoft.utils.Keymap.K_KP_2; +import static com.glavsoft.utils.Keymap.K_KP_3; +import static com.glavsoft.utils.Keymap.K_KP_4; +import static com.glavsoft.utils.Keymap.K_KP_5; +import static com.glavsoft.utils.Keymap.K_KP_6; +import static com.glavsoft.utils.Keymap.K_KP_7; +import static com.glavsoft.utils.Keymap.K_KP_8; +import static com.glavsoft.utils.Keymap.K_KP_9; +import static com.glavsoft.utils.Keymap.K_KP_ADD; +import static com.glavsoft.utils.Keymap.K_KP_DECIMAL; +import static com.glavsoft.utils.Keymap.K_KP_DIVIDE; +import static com.glavsoft.utils.Keymap.K_KP_DOWN; +import static com.glavsoft.utils.Keymap.K_KP_END; +import static com.glavsoft.utils.Keymap.K_KP_ENTER; +import static com.glavsoft.utils.Keymap.K_KP_HOME; +import static com.glavsoft.utils.Keymap.K_KP_INSERT; +import static com.glavsoft.utils.Keymap.K_KP_LEFT; +import static com.glavsoft.utils.Keymap.K_KP_MULTIPLY; +import static com.glavsoft.utils.Keymap.K_KP_PAGE_DOWN; +import static com.glavsoft.utils.Keymap.K_KP_PAGE_UP; +import static com.glavsoft.utils.Keymap.K_KP_RIGHT; +import static com.glavsoft.utils.Keymap.K_KP_SEPARATOR; +import static com.glavsoft.utils.Keymap.K_KP_SUBTRACT; +import static com.glavsoft.utils.Keymap.K_KP_UP; +import static com.glavsoft.utils.Keymap.K_LEFT; +import static com.glavsoft.utils.Keymap.K_META_LEFT; +import static com.glavsoft.utils.Keymap.K_PAGE_DOWN; +import static com.glavsoft.utils.Keymap.K_PAGE_UP; +import static com.glavsoft.utils.Keymap.K_RIGHT; +import static com.glavsoft.utils.Keymap.K_SHIFT_LEFT; +import static com.glavsoft.utils.Keymap.K_SUPER_LEFT; +import static com.glavsoft.utils.Keymap.K_TAB; +import static com.glavsoft.utils.Keymap.K_UP; +import static com.glavsoft.utils.Keymap.unicode2keysym; + +public class KeyEventListener implements KeyListener { + + + private ModifierButtonEventListener modifierButtonListener; + private boolean convertToAscii; + private final Protocol protocol; + private KeyboardConvertor convertor; + + public KeyEventListener(Protocol protocol) { + this.protocol = protocol; + this.convertToAscii = false; + } + + private void processKeyEvent(KeyEvent e) { + if (processModifierKeys(e)) return; + if (processSpecialKeys(e)) return; + if (processActionKey(e)) return; + + int keyChar = e.getKeyChar(); + final int location = e.getKeyLocation(); + if (0xffff == keyChar) { keyChar = convertToAscii? convertor.convert(keyChar, e) : 0; } + if (keyChar < 0x20) { + if (e.isControlDown() && keyChar != e.getKeyCode()) { + keyChar += 0x60; // to differ Ctrl-H from Ctrl-Backspace + } else { + switch (keyChar) { + case KeyEvent.VK_BACK_SPACE: keyChar = K_BACK_SPACE; break; + case KeyEvent.VK_TAB: keyChar = K_TAB; break; + case KeyEvent.VK_ESCAPE: keyChar = K_ESCAPE; break; + case KeyEvent.VK_ENTER: + keyChar = KeyEvent.KEY_LOCATION_NUMPAD == location ? K_KP_ENTER : K_ENTER; + break; + default: break; //fall through + } + + } + } else if (KeyEvent.VK_DELETE == keyChar) { + keyChar = K_DELETE; + } else if (convertToAscii) { + keyChar = convertor.convert(keyChar, e); + } else { + keyChar = unicode2keysym(keyChar); + } + + sendKeyEvent(keyChar, e); + } + + + /** + * Process AltGraph, num pad keys... + */ + private boolean processSpecialKeys(KeyEvent e) { + int keyCode = e.getKeyCode(); + if (KeyEvent.VK_ALT_GRAPH == keyCode) { + sendKeyEvent(K_CTRL_LEFT, e); + sendKeyEvent(K_ALT_LEFT, e); + return true; + } + switch (keyCode) { + case KeyEvent.VK_NUMPAD0: keyCode = K_KP_0;break; + case KeyEvent.VK_NUMPAD1: keyCode = K_KP_1;break; + case KeyEvent.VK_NUMPAD2: keyCode = K_KP_2;break; + case KeyEvent.VK_NUMPAD3: keyCode = K_KP_3;break; + case KeyEvent.VK_NUMPAD4: keyCode = K_KP_4;break; + case KeyEvent.VK_NUMPAD5: keyCode = K_KP_5;break; + case KeyEvent.VK_NUMPAD6: keyCode = K_KP_6;break; + case KeyEvent.VK_NUMPAD7: keyCode = K_KP_7;break; + case KeyEvent.VK_NUMPAD8: keyCode = K_KP_8;break; + case KeyEvent.VK_NUMPAD9: keyCode = K_KP_9;break; + + case KeyEvent.VK_MULTIPLY: keyCode = K_KP_MULTIPLY;break; + case KeyEvent.VK_ADD: keyCode = K_KP_ADD;break; + case KeyEvent.VK_SEPARATOR: keyCode = K_KP_SEPARATOR;break; + case KeyEvent.VK_SUBTRACT: keyCode = K_KP_SUBTRACT;break; + case KeyEvent.VK_DECIMAL: keyCode = K_KP_DECIMAL;break; + case KeyEvent.VK_DIVIDE: keyCode = K_KP_DIVIDE;break; + + default: return false; + } + sendKeyEvent(keyCode, e); + return true; + } + + private boolean processActionKey(KeyEvent e) { + int keyCode = e.getKeyCode(); + final int location = e.getKeyLocation(); + if (e.isActionKey()) { + switch (keyCode) { + case KeyEvent.VK_HOME: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_HOME: K_HOME; break; + case KeyEvent.VK_LEFT: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_LEFT: K_LEFT; break; + case KeyEvent.VK_UP: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_UP: K_UP; break; + case KeyEvent.VK_RIGHT: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_RIGHT: K_RIGHT; break; + case KeyEvent.VK_DOWN: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_DOWN: K_DOWN; break; + case KeyEvent.VK_PAGE_UP: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_PAGE_UP: K_PAGE_UP; break; + case KeyEvent.VK_PAGE_DOWN: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_PAGE_DOWN: K_PAGE_DOWN; break; + case KeyEvent.VK_END: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_END: K_END; break; + case KeyEvent.VK_INSERT: keyCode = KeyEvent.KEY_LOCATION_NUMPAD == location? K_KP_INSERT: K_INSERT; break; + case KeyEvent.VK_F1: keyCode = K_F1; break; + case KeyEvent.VK_F2: keyCode = K_F2; break; + case KeyEvent.VK_F3: keyCode = K_F3; break; + case KeyEvent.VK_F4: keyCode = K_F4; break; + case KeyEvent.VK_F5: keyCode = K_F5; break; + case KeyEvent.VK_F6: keyCode = K_F6; break; + case KeyEvent.VK_F7: keyCode = K_F7; break; + case KeyEvent.VK_F8: keyCode = K_F8; break; + case KeyEvent.VK_F9: keyCode = K_F9; break; + case KeyEvent.VK_F10: keyCode = K_F10; break; + case KeyEvent.VK_F11: keyCode = K_F11; break; + case KeyEvent.VK_F12: keyCode = K_F12; break; + + case KeyEvent.VK_KP_LEFT: keyCode = K_KP_LEFT; break; + case KeyEvent.VK_KP_UP: keyCode = K_KP_UP; break; + case KeyEvent.VK_KP_RIGHT: keyCode = K_KP_RIGHT; break; + case KeyEvent.VK_KP_DOWN: keyCode = K_KP_DOWN; break; + + default: return false; // ignore other 'action' keys + } + sendKeyEvent(keyCode, e); + return true; + } + return false; + } + + private boolean processModifierKeys(KeyEvent e) { + int keyCode = e.getKeyCode(); + switch (keyCode) { + case KeyEvent.VK_CONTROL: keyCode = K_CTRL_LEFT; break; + case KeyEvent.VK_SHIFT: keyCode = K_SHIFT_LEFT; break; + case KeyEvent.VK_ALT: keyCode = K_ALT_LEFT; break; + case KeyEvent.VK_META: keyCode = K_META_LEFT; break; + // follow two are 'action' keys in java terms but modifier keys actualy + case KeyEvent.VK_WINDOWS: keyCode = K_SUPER_LEFT; break; + case KeyEvent.VK_CONTEXT_MENU: keyCode = K_HYPER_LEFT; break; + default: return false; + } + if (modifierButtonListener != null) { + modifierButtonListener.fireEvent(e); + } + sendKeyEvent(keyCode + + (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT ? 1 : 0), // "Right" Ctrl/Alt/Shift/Meta deffers frim "Left" ones by +1 + e); + return true; + } + + private void sendKeyEvent(int keyChar, KeyEvent e) { + protocol.sendMessage(new KeyEventMessage(keyChar, e.getID() == KeyEvent.KEY_PRESSED)); + } + + @Override + public void keyTyped(KeyEvent e) { + e.consume(); + } + + @Override + public void keyPressed(KeyEvent e) { + processKeyEvent(e); + e.consume(); + } + + @Override + public void keyReleased(KeyEvent e) { + processKeyEvent(e); + e.consume(); + } + + public void addModifierListener(ModifierButtonEventListener modifierButtonListener) { + this.modifierButtonListener = modifierButtonListener; + } + + public void setConvertToAscii(boolean convertToAscii) { + this.convertToAscii = convertToAscii; + if (convertToAscii && null == convertor) { + convertor = new KeyboardConvertor(); + } + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyboardConvertor.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyboardConvertor.java new file mode 100644 index 0000000..7a152cf --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/KeyboardConvertor.java @@ -0,0 +1,150 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import java.awt.Toolkit; +import java.awt.event.KeyEvent; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class KeyboardConvertor { + private static final boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + private static final String PATTERN_STRING_FOR_SCANCODE = "scancode=(\\d+)"; + private Pattern patternForScancode; + @SuppressWarnings("serial") + private static final Map keyMap = new HashMap() {{ + put(192 /* Back Quote */, new CodePair('`' /*96*/, '~' /*126*/)); + put(49 /* 1 */, new CodePair('1' /*49*/, '!' /*33*/)); + put(50 /* 2 */, new CodePair('2' /*50*/, '@' /*64*/)); + put(51 /* 3 */, new CodePair('3' /*51*/, '#' /*35*/)); + put(52 /* 4 */, new CodePair('4' /*52*/, '$' /*36*/)); + put(53 /* 5 */, new CodePair('5' /*53*/, '%' /*37*/)); + put(54 /* 6 */, new CodePair('6' /*54*/, '^' /*94*/)); + put(55 /* 7 */, new CodePair('7' /*55*/, '&' /*38*/)); + put(56 /* 8 */, new CodePair('8' /*56*/, '*' /*42*/)); + put(57 /* 9 */, new CodePair('9' /*57*/, '(' /*40*/)); + put(48 /* 0 */, new CodePair('0' /*48*/, ')' /*41*/)); + put(45 /* Minus */, new CodePair('-' /*45*/, '_' /*95*/)); + put(61 /* Equals */, new CodePair('=' /*61*/, '+' /*43*/)); + put(92 /* Back Slash */, new CodePair('\\' /*92*/, '|' /*124*/)); + + put(81 /* Q */, new CodePair('q' /*113*/, 'Q' /*81*/)); + put(87 /* W */, new CodePair('w' /*119*/, 'W' /*87*/)); + put(69 /* E */, new CodePair('e' /*101*/, 'E' /*69*/)); + put(82 /* R */, new CodePair('r' /*114*/, 'R' /*82*/)); + put(84 /* T */, new CodePair('t' /*116*/, 'T' /*84*/)); + put(89 /* Y */, new CodePair('y' /*121*/, 'Y' /*89*/)); + put(85 /* U */, new CodePair('u' /*117*/, 'U' /*85*/)); + put(73 /* I */, new CodePair('i' /*105*/, 'I' /*73*/)); + put(79 /* O */, new CodePair('o' /*111*/, 'O' /*79*/)); + put(80 /* P */, new CodePair('p' /*112*/, 'P' /*80*/)); + put(91 /* Open Bracket */, new CodePair('[' /*91*/, '{' /*123*/)); + put(93 /* Close Bracket */, new CodePair(']' /*93*/, '}' /*125*/)); + + put(65 /* A */, new CodePair('a' /*97*/, 'A' /*65*/)); + put(83 /* S */, new CodePair('s' /*115*/, 'S' /*83*/)); + put(68 /* D */, new CodePair('d' /*100*/, 'D' /*68*/)); + put(70 /* F */, new CodePair('f' /*102*/, 'F' /*70*/)); + put(71 /* G */, new CodePair('g' /*103*/, 'G' /*71*/)); + put(72 /* H */, new CodePair('h' /*104*/, 'H' /*72*/)); + put(74 /* J */, new CodePair('j' /*106*/, 'J' /*74*/)); + put(75 /* K */, new CodePair('k' /*107*/, 'K' /*75*/)); + put(76 /* L */, new CodePair('l' /*108*/, 'L' /*76*/)); + put(59 /* Semicolon */, new CodePair(';' /*59*/, ':' /*58*/)); + put(222 /* Quote */, new CodePair('\'' /*39*/, '"' /*34*/)); + + put(90 /* Z */, new CodePair('z' /*122*/, 'Z' /*90*/)); + put(88 /* X */, new CodePair('x' /*120*/, 'X' /*88*/)); + put(67 /* C */, new CodePair('c' /*99*/, 'C' /*67*/)); + put(86 /* V */, new CodePair('v' /*118*/, 'V' /*86*/)); + put(66 /* B */, new CodePair('b' /*98*/, 'B' /*66*/)); + put(78 /* N */, new CodePair('n' /*110*/, 'N' /*78*/)); + put(77 /* M */, new CodePair('m' /*109*/, 'M' /*77*/)); + put(44 /* Comma */, new CodePair(',' /*44*/, '<' /*60*/)); + put(46 /* Period */, new CodePair('.' /*46*/, '>' /*62*/)); + put(47 /* Slash */, new CodePair('/' /*47*/, '?' /*63*/)); + +// put(60 /* Less */, new CodePair('<' /*60*/, '>')); // 105-th key on 105-keys keyboard (less/greather/bar) + put(KeyEvent.VK_LESS /* Less */, new CodePair('<' /*60*/, '>')); // 105-th key on 105-keys keyboard (less/greather/bar) +// put(KeyEvent.VK_GREATER /* Greater */, new CodePair('<' /*60*/, '>')); // 105-th key on 105-keys keyboard (less/greather/bar) + }}; + + private boolean canCheckCapsWithToolkit; + + public KeyboardConvertor() { + try { + Toolkit.getDefaultToolkit().getLockingKeyState(KeyEvent.VK_CAPS_LOCK); + canCheckCapsWithToolkit = true; + } catch (Exception e) { + canCheckCapsWithToolkit = false; + } + if (isWindows) { + patternForScancode = Pattern.compile(PATTERN_STRING_FOR_SCANCODE); + } + } + + public int convert(int keyChar, KeyEvent ev) { + int keyCode = ev.getKeyCode(); + boolean isShiftDown = ev.isShiftDown(); + CodePair codePair = keyMap.get(keyCode); + if (null == codePair) + return keyChar; + if (isWindows) { + final Matcher matcher = patternForScancode.matcher(ev.paramString()); + if (matcher.matches()) { + try { + int scancode = Integer.parseInt(matcher.group(1)); + if (90 == keyCode && 21 == scancode) { // deutsch z->y + codePair = keyMap.get(89); // y + } else if (89 == keyCode && 44 == scancode) { // deutsch y->z + codePair = keyMap.get(90); // z + } + } catch (NumberFormatException e) { /*nop*/ } + } + } + boolean isCapsLock = false; + if (Character.isLetter(codePair.code)) { + if (canCheckCapsWithToolkit) { + try { + isCapsLock = + Toolkit.getDefaultToolkit().getLockingKeyState(KeyEvent.VK_CAPS_LOCK); + } catch (Exception ex) { /* nop */ } + } + } + return isShiftDown && ! isCapsLock || ! isShiftDown && isCapsLock ? + codePair.codeShifted : + codePair.code; + } + + private static class CodePair { + public int code, codeShifted; + public CodePair(int code, int codeShifted) { + this.code = code; + this.codeShifted = codeShifted; + } + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ModifierButtonEventListener.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ModifierButtonEventListener.java new file mode 100644 index 0000000..905b967 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ModifierButtonEventListener.java @@ -0,0 +1,42 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import javax.swing.JToggleButton; +import java.awt.event.KeyEvent; +import java.util.HashMap; +import java.util.Map; + +public class ModifierButtonEventListener { + Map buttons = new HashMap(); + public void addButton(int keyCode, JToggleButton button) { + buttons.put(keyCode, button); + } + public void fireEvent(KeyEvent e) { + int code = e.getKeyCode(); + if (buttons.containsKey(code)) { + buttons.get(code).setSelected(e.getID() == KeyEvent.KEY_PRESSED); + } + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEnteredListener.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEnteredListener.java new file mode 100644 index 0000000..f2eedb4 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEnteredListener.java @@ -0,0 +1,33 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import java.awt.event.MouseEvent; + +/** + * @author dime at glavsoft.com + */ +public interface MouseEnteredListener { + void mouseEnteredEvent(MouseEvent mouseEvent); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEventListener.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEventListener.java new file mode 100644 index 0000000..8ccaa16 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/MouseEventListener.java @@ -0,0 +1,123 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.rfb.IRepaintController; +import com.glavsoft.rfb.client.PointerEventMessage; +import com.glavsoft.rfb.protocol.Protocol; + +import javax.swing.event.MouseInputAdapter; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +public class MouseEventListener extends MouseInputAdapter + implements MouseWheelListener { + private static final byte BUTTON_LEFT = 1; + private static final byte BUTTON_MIDDLE = 1 << 1; + private static final byte BUTTON_RIGHT = 1 << 2; + private static final byte WHEEL_UP = 1 << 3; + private static final byte WHEEL_DOWN = 1 << 4; + + private final IRepaintController repaintController; + private final Protocol protocol; + private volatile double scaleFactor; + + public MouseEventListener(IRepaintController repaintController, Protocol protocol, + double scaleFactor) { + this.repaintController = repaintController; + this.protocol = protocol; + this.scaleFactor = scaleFactor; + } + + public void processMouseEvent(MouseEvent mouseEvent, + MouseWheelEvent mouseWheelEvent, boolean moved) { + byte buttonMask = 0; + if (null == mouseEvent && mouseWheelEvent != null) { + mouseEvent = mouseWheelEvent; + } + assert mouseEvent != null; + short x = (short) (mouseEvent.getX() / scaleFactor); + short y = (short) (mouseEvent.getY() / scaleFactor); + if (moved) { + repaintController.updateCursorPosition(x, y); + } + + int modifiersEx = mouseEvent.getModifiersEx(); + // left + buttonMask |= (modifiersEx & InputEvent.BUTTON1_DOWN_MASK) != 0 ? + BUTTON_LEFT : 0; + // middle + buttonMask |= (modifiersEx & InputEvent.BUTTON2_DOWN_MASK) != 0 ? + BUTTON_MIDDLE : 0; + // right + buttonMask |= (modifiersEx & InputEvent.BUTTON3_DOWN_MASK) != 0 ? + BUTTON_RIGHT : 0; + + // wheel + if (mouseWheelEvent != null) { + int notches = mouseWheelEvent.getWheelRotation(); + byte wheelMask = notches < 0 ? WHEEL_UP : WHEEL_DOWN; + // handle more then 1 notches + notches = Math.abs(notches); + for (int i = 1; i < notches; ++i) { + protocol.sendMessage(new PointerEventMessage((byte) (buttonMask | wheelMask), x, y)); + protocol.sendMessage(new PointerEventMessage(buttonMask, x, y)); + } + protocol.sendMessage(new PointerEventMessage((byte) (buttonMask | wheelMask), x, y)); + } + protocol.sendMessage(new PointerEventMessage(buttonMask, x, y)); + } + + @Override + public void mousePressed(MouseEvent mouseEvent) { + mouseEvent.getComponent().requestFocusInWindow(); + processMouseEvent(mouseEvent, null, false); + } + + @Override + public void mouseReleased(MouseEvent mouseEvent) { + processMouseEvent(mouseEvent, null, false); + } + + @Override + public void mouseDragged(MouseEvent mouseEvent) { + processMouseEvent(mouseEvent, null, true); + } + + @Override + public void mouseMoved(MouseEvent mouseEvent) { + processMouseEvent(mouseEvent, null, true); + } + + @Override + public void mouseWheelMoved(MouseWheelEvent emouseWheelEvent) { + processMouseEvent(null, emouseWheelEvent, false); + } + + public void setScaleFactor(double scaleFactor) { + this.scaleFactor = scaleFactor; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/RendererImpl.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/RendererImpl.java new file mode 100644 index 0000000..8329b9e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/RendererImpl.java @@ -0,0 +1,138 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.drawing.Renderer; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.rfb.encoding.decoder.FramebufferUpdateRectangle; +import com.glavsoft.transport.Transport; + +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferInt; +import java.awt.image.DirectColorModel; +import java.awt.image.ImageObserver; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class RendererImpl extends Renderer implements ImageObserver { + CyclicBarrier barrier = new CyclicBarrier(2); + private final Image offscreenImage; + + public RendererImpl(Transport transport, int width, int height, PixelFormat pixelFormat) { + if (0 == width) width = 1; + if (0 == height) height = 1; + init(width, height, pixelFormat); + ColorModel colorModel = new DirectColorModel(24, 0xff0000, 0xff00, 0xff); + SampleModel sampleModel = colorModel.createCompatibleSampleModel(width, + height); + + DataBuffer dataBuffer = new DataBufferInt(pixels, width * height); + WritableRaster raster = Raster.createWritableRaster(sampleModel, + dataBuffer, null); + offscreenImage = new BufferedImage(colorModel, raster, false, null); + cursor = new SoftCursorImpl(0, 0, 0, 0); + } + + /** + * Draw jpeg image data + * + * @param bytes jpeg image data array + * @param offset start offset at data array + * @param jpegBufferLength jpeg image data array length + * @param rect image location and dimensions + */ + @Override + public void drawJpegImage(byte[] bytes, int offset, int jpegBufferLength, + FramebufferUpdateRectangle rect) { + Image jpegImage = Toolkit.getDefaultToolkit().createImage(bytes, + offset, jpegBufferLength); + Toolkit.getDefaultToolkit().prepareImage(jpegImage, -1, -1, this); + try { + barrier.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // nop + } catch (BrokenBarrierException e) { + // nop + } catch (TimeoutException e) { + // nop + } + Graphics graphics = offscreenImage.getGraphics(); + graphics.drawImage(jpegImage, rect.x, rect.y, rect.width, rect.height, this); + } + + @Override + public boolean imageUpdate(Image img, int infoflags, int x, int y, + int width, int height) { + boolean isReady = (infoflags & (ALLBITS | ABORT)) != 0; + if (isReady) { + try { + barrier.await(); + } catch (InterruptedException e) { + // nop + } catch (BrokenBarrierException e) { + // nop + } + } + return !isReady; + } + + /* Swing specific interface */ + public Image getOffscreenImage() { + return offscreenImage; + } + + public void paintImageOn(Graphics g, int width, int height, double scaleFactor) { + lock.lock(); + try { + g.drawImage(offscreenImage, 0, 0, null); + } finally { + lock.unlock(); + } + } + + public void paintCursorOn(Graphics g, boolean force) { + synchronized (cursor.getLock()) { + Image cursorImage = ((SoftCursorImpl) cursor).getImage(); + if (cursorImage != null && (force || + g.getClipBounds().intersects(cursor.rX, cursor.rY, cursor.width, cursor.height))) { + g.drawImage(cursorImage, cursor.rX, cursor.rY, null); + } + } + } + + public SoftCursorImpl getCursor() { + return (SoftCursorImpl) cursor; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SoftCursorImpl.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SoftCursorImpl.java new file mode 100644 index 0000000..862d9da --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SoftCursorImpl.java @@ -0,0 +1,50 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.drawing.SoftCursor; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.MemoryImageSource; + +public class SoftCursorImpl extends SoftCursor { + private Image cursorImage; + + public SoftCursorImpl(int hotX, int hotY, int width, int height) { + super(hotX, hotY, width, height); + } + + public Image getImage() { + return cursorImage; + } + + @Override + protected void createNewCursorImage(int[] cursorPixels, int hotX, int hotY, int width, int height) { + cursorImage = Toolkit.getDefaultToolkit().createImage( + new MemoryImageSource(width, height, cursorPixels, 0, width)); + + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Surface.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Surface.java new file mode 100644 index 0000000..faf00ae --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Surface.java @@ -0,0 +1,272 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.core.SettingsChangedEvent; +import com.glavsoft.drawing.Renderer; +import com.glavsoft.rfb.IChangeSettingsListener; +import com.glavsoft.rfb.IRepaintController; +import com.glavsoft.rfb.encoding.PixelFormat; +import com.glavsoft.rfb.encoding.decoder.FramebufferUpdateRectangle; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.transport.Transport; +import com.glavsoft.viewer.settings.LocalMouseCursorShape; +import com.glavsoft.viewer.settings.UiSettings; + +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Logger; + +public class Surface extends JPanel implements IRepaintController, IChangeSettingsListener { + + private int width; + private int height; + private SoftCursorImpl cursor; + private volatile RendererImpl renderer; + private MouseEventListener mouseEventListener; + private KeyEventListener keyEventListener; + private boolean showCursor; + private ModifierButtonEventListener modifierButtonListener; + private boolean isUserInputEnabled = false; + private final Protocol protocol; + private double scaleFactor; + public Dimension oldSize; + + @Override + public boolean isDoubleBuffered() { + // TODO returning false in some reason may speed ups drawing, but may + // not. Needed in challenging. + return false; + } + + public Surface(Protocol protocol, double scaleFactor, LocalMouseCursorShape mouseCursorShape) { + this.protocol = protocol; + this.scaleFactor = scaleFactor; + this.setFocusable(true); + init(protocol.getFbWidth(), protocol.getFbHeight()); + oldSize = getPreferredSize(); + + if (!protocol.getSettings().isViewOnly()) { + setUserInputEnabled(true, protocol.getSettings().isConvertToAscii()); + } + showCursor = protocol.getSettings().isShowRemoteCursor(); + setLocalCursorShape(mouseCursorShape); + } + + public void setViewerWindow(SwingViewerWindow viewerWindow) { + throw new UnsupportedOperationException("Not supported yet."); + } + + private void setUserInputEnabled(boolean enable, boolean convertToAscii) { + if (enable == isUserInputEnabled) return; + isUserInputEnabled = enable; + if (enable) { + if (null == mouseEventListener) { + mouseEventListener = new MouseEventListener(this, protocol, scaleFactor); + } + addMouseListener(mouseEventListener); + addMouseMotionListener(mouseEventListener); + addMouseWheelListener(mouseEventListener); + + setFocusTraversalKeysEnabled(false); + if (null == keyEventListener) { + keyEventListener = new KeyEventListener(protocol); + if (modifierButtonListener != null) { + keyEventListener.addModifierListener(modifierButtonListener); + } + } + keyEventListener.setConvertToAscii(convertToAscii); + addKeyListener(keyEventListener); + enableInputMethods(false); + } else { + removeMouseListener(mouseEventListener); + removeMouseMotionListener(mouseEventListener); + removeMouseWheelListener(mouseEventListener); + removeKeyListener(keyEventListener); + } + } + + @Override + public Renderer createRenderer(Transport transport, int width, int height, PixelFormat pixelFormat) { + renderer = new RendererImpl(transport, width, height, pixelFormat); + cursor = renderer.getCursor(); + if (SwingUtilities.isEventDispatchThread()) { + init(renderer.getWidth(), renderer.getHeight()); + updateFrameSize(); + } else { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + init(renderer.getWidth(), renderer.getHeight()); + updateFrameSize(); + } + }); + } catch (InterruptedException e) { + Logger.getLogger(getClass().getName()).severe("Interrupted: " + e.getMessage()); + protocol.cleanUpSession("Interrupted: " + e.getMessage()); + } catch (InvocationTargetException e) { + Logger.getLogger(getClass().getName()).severe("Fatal error: " + e.getCause().getMessage()); + protocol.cleanUpSession("Fatal error: " + e.getCause().getMessage()); + } + } + return renderer; + } + + private void init(int width, int height) { + this.width = width; + this.height = height; + setSize(getPreferredSize()); + } + + private void updateFrameSize() { + setSize(getPreferredSize()); +// viewerWindow.pack(); + requestFocus(); + } + + @Override + public void paintComponent(Graphics g) { // EDT + if (null == renderer) return; + + if (scaleFactor != 1.0) { + ((Graphics2D) g).scale(scaleFactor, scaleFactor); + } + + + final Object appleContentScaleFactor = getToolkit().getDesktopProperty("apple.awt.contentScaleFactor"); + ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_RENDERING, + (appleContentScaleFactor != null && (Integer) appleContentScaleFactor != 1) ? + RenderingHints.VALUE_RENDER_SPEED : // speed for Apple Retina display + RenderingHints.VALUE_RENDER_DEFAULT); // quality for others + renderer.paintImageOn(g, super.getWidth(), super.getHeight(),scaleFactor); // internally locked with renderer.lock + if (showCursor) { + renderer.paintCursorOn(g, scaleFactor != 1);// internally locked with cursor.lock + } + } + + @Override + public Dimension getPreferredSize() { + return new Dimension((int) (this.width * scaleFactor), (int) (this.height * scaleFactor)); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + /** + * Saves protocol and simply invokes native JPanel repaint method which + * asyncroniously register repaint request using invokeLater to repaint be + * runned in Swing event dispatcher thread. So may be called from other + * threads. + */ + @Override + public void repaintBitmap(FramebufferUpdateRectangle rect) { + repaintBitmap(rect.x, rect.y, rect.width, rect.height); + } + + @Override + public void repaintBitmap(int x, int y, int width, int height) { + repaint((int) (x * scaleFactor), (int) (y * scaleFactor), + (int) Math.ceil(width * scaleFactor), (int) Math.ceil(height * scaleFactor)); + } + + @Override + public void repaintCursor() { + synchronized (cursor.getLock()) { + repaint((int) (cursor.oldRX * scaleFactor), (int) (cursor.oldRY * scaleFactor), + (int) Math.ceil(cursor.oldWidth * scaleFactor) + 1, (int) Math.ceil(cursor.oldHeight * scaleFactor) + 1); + repaint((int) (cursor.rX * scaleFactor), (int) (cursor.rY * scaleFactor), + (int) Math.ceil(cursor.width * scaleFactor) + 1, (int) Math.ceil(cursor.height * scaleFactor) + 1); + } + } + + @Override + public void updateCursorPosition(short x, short y) { + synchronized (cursor.getLock()) { + cursor.updatePosition(x, y); + repaintCursor(); + } + } + + private void showCursor(boolean show) { + synchronized (cursor.getLock()) { + showCursor = show; + } + } + + public void addModifierListener(ModifierButtonEventListener modifierButtonListener) { + this.modifierButtonListener = modifierButtonListener; + if (keyEventListener != null) { + keyEventListener.addModifierListener(modifierButtonListener); + } + } + + @Override + public void settingsChanged(SettingsChangedEvent e) { + if (ProtocolSettings.isRfbSettingsChangedFired(e)) { + ProtocolSettings settings = (ProtocolSettings) e.getSource(); + setUserInputEnabled(!settings.isViewOnly(), settings.isConvertToAscii()); + showCursor(settings.isShowRemoteCursor()); + } else if (UiSettings.isUiSettingsChangedFired(e)) { + UiSettings uiSettings = (UiSettings) e.getSource(); + oldSize = getPreferredSize(); + scaleFactor = uiSettings.getScaleFactor(); + if (uiSettings.isChangedMouseCursorShape()) { + setLocalCursorShape(uiSettings.getMouseCursorShape()); + } + } + mouseEventListener.setScaleFactor(scaleFactor); + updateFrameSize(); + } + + public void setLocalCursorShape(LocalMouseCursorShape cursorShape) { + if (LocalMouseCursorShape.SYSTEM_DEFAULT == cursorShape) { + setCursor(Cursor.getDefaultCursor()); + } else { + setCursor(Utils.getCursor(cursorShape)); + } + } + + @Override + public void setPixelFormat(PixelFormat pixelFormat) { + if (renderer != null) { + renderer.initColorDecoder(pixelFormat); + } + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingConnectionWorkerFactory.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingConnectionWorkerFactory.java new file mode 100644 index 0000000..6b25484 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingConnectionWorkerFactory.java @@ -0,0 +1,69 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.viewer.mvp.Presenter; +import com.glavsoft.viewer.workers.AbstractConnectionWorkerFactory; +import com.glavsoft.viewer.workers.NetworkConnectionWorker; +import com.glavsoft.viewer.workers.RfbConnectionWorker; + +import java.awt.Component; + +/** + * @author dime at tightvnc.com + */ +public class SwingConnectionWorkerFactory extends AbstractConnectionWorkerFactory { + + private Component parent; + private String predefinedPassword; + private final ConnectionPresenter presenter; + private final SwingViewerWindowFactory viewerWindowFactory; + + public SwingConnectionWorkerFactory(Component parent, String predefinedPassword, Presenter presenter, + SwingViewerWindowFactory viewerWindowFactory) { + this.parent = parent; + this.predefinedPassword = predefinedPassword; + this.presenter = (ConnectionPresenter) presenter; + this.viewerWindowFactory = viewerWindowFactory; + } + + public SwingConnectionWorkerFactory(Component parent, Presenter connectionPresenter, SwingViewerWindowFactory viewerWindowFactory) { + this(parent, "", connectionPresenter, viewerWindowFactory); + } + + @Override + public NetworkConnectionWorker createNetworkConnectionWorker() { + return new SwingNetworkConnectionWorker(parent); + } + + @Override + public RfbConnectionWorker createRfbConnectionWorker() { + return new SwingRfbConnectionWorker(predefinedPassword, presenter, parent, viewerWindowFactory); + } + + @Override + public void setPredefinedPassword(String predefinedPassword) { + this.predefinedPassword = predefinedPassword; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingNetworkConnectionWorker.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingNetworkConnectionWorker.java new file mode 100644 index 0000000..0383756 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingNetworkConnectionWorker.java @@ -0,0 +1,199 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.viewer.mvp.Presenter; +import com.glavsoft.viewer.settings.ConnectionParams; +import com.glavsoft.viewer.swing.ssh.SshConnectionManager; +import com.glavsoft.viewer.workers.NetworkConnectionWorker; + +import javax.swing.SwingWorker; +import java.awt.Component; +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + + +public class SwingNetworkConnectionWorker extends SwingWorker implements NetworkConnectionWorker { + public static final int MAX_HOSTNAME_LENGTH_FOR_MESSAGES = 40; + private final Component parent; + private Logger logger; + private boolean hasSshSupport; + private ConnectionParams connectionParams; + private ConnectionPresenter presenter; + private SshConnectionManager sshConnectionManager; + + + public SwingNetworkConnectionWorker(Component parent) { + this.parent = parent; + logger = Logger.getLogger(getClass().getName()); + } + + @Override + public Socket doInBackground() throws Exception { + String s = "" +connectionParams.hostName + ":" + connectionParams.getPortNumber(); + if (connectionParams.useSsh()) { + s += " (via com.glavsoft.viewer.swing.ssh://" + connectionParams.sshUserName + "@" + connectionParams.sshHostName + ":" + connectionParams.getSshPortNumber() + ")"; + } + + String message = "Trying to connect to " + s + ""; + logger.info(message.replaceAll("<[^<>]+?>", "")); + publish(message); + int port; + String host; + if (hasSshSupport && connectionParams.useSsh()) { + try { + sshConnectionManager = SshConnectionManager.createManager(parent); + } catch (ConnectionErrorException e) { + hasSshSupport = false; // TODO propogate into upper level? + throw e; + } + message = "Creating SSH tunnel to " + connectionParams.sshHostName + ":" + connectionParams.getSshPortNumber(); + logger.info(message); + publish(message); + port = sshConnectionManager.connect(connectionParams); + if (sshConnectionManager.isConnected() ) { + host = "127.0.0.1"; + message = "SSH tunnel established: " + host + ":" + port; + logger.info(message); + publish(message); + } else { + throw new ConnectionErrorException("Could not create SSH tunnel: " + sshConnectionManager.getErrorMessage()); + } + } else { + host = connectionParams.hostName; + port = connectionParams.getPortNumber(); + } + + message = "Connecting to host " + host + ":" + port + (connectionParams.useSsh() ? " (tunneled)" : ""); + logger.info(message); + publish(message); + + return new Socket(host, port); + } + + private String formatHostString(String hostName) { + if (hostName.length() <= MAX_HOSTNAME_LENGTH_FOR_MESSAGES) { + return hostName; + } else { + return hostName.substring(0, MAX_HOSTNAME_LENGTH_FOR_MESSAGES) + "..."; + } + } + + @Override + protected void process(List strings) { // EDT + String message = strings.get(strings.size() - 1); // get last + presenter.showMessage(message); + } + + @Override + protected void done() { // EDT + try { + final Socket socket = get(); + presenter.successfulNetworkConnection(socket); + } catch (CancellationException e) { + logger.info("Cancelled: " + e.getMessage()); + e.printStackTrace(); + presenter.showMessage("Cancelled"); + presenter.connectionFailed(); + } catch (InterruptedException e) { + logger.info("Interrupted"); + presenter.showMessage("Interrupted"); + presenter.connectionFailed(); + } catch (ExecutionException e) { + String errorMessage = null; + try { + throw e.getCause(); + } catch (UnknownHostException uhe) { + logger.severe("Unknown host: " + connectionParams.hostName); + errorMessage = "Unknown host: '" + formatHostString(connectionParams.hostName) + "'"; + } catch (IOException ioe) { + logger.severe("Couldn't connect to '" + connectionParams.hostName + + ":" + connectionParams.getPortNumber() + "':\n" + ioe.getMessage()); + logger.log(Level.FINEST, "Couldn't connect to '" + connectionParams.hostName + + ":" + connectionParams.getPortNumber() + "':\n" + ioe.getMessage(), ioe); + errorMessage = "Couldn't connect to '" + formatHostString(connectionParams.hostName) + + ":" + connectionParams.getPortNumber() + "':\n" + ioe.getMessage(); + } catch (CancelConnectionQuietlyException cce) { + logger.warning("Cancelled by user: " + cce.getMessage()); +// errorMessage = null; // exit without dialog showing + } catch (CancelConnectionException cce) { + logger.severe("Cancelled: " + cce.getMessage()); + errorMessage = cce.getMessage(); + } catch (SecurityException ace) { + logger.severe("Couldn't connect to: " + + connectionParams.hostName + ":" + connectionParams.getPortNumber() + + ": " + ace.getMessage()); + logger.log(Level.FINEST, "Couldn't connect to: " + + connectionParams.hostName + ":" + connectionParams.getPortNumber() + + ": " + ace.getMessage(), ace); + errorMessage = "Access control error"; + } catch (ConnectionErrorException cee) { + logger.severe(cee.getMessage() + " host: " + + connectionParams.hostName + ":" + connectionParams.getPortNumber()); + errorMessage = cee.getMessage() + "\nHost: " + + formatHostString(connectionParams.hostName) + ":" + connectionParams.getPortNumber(); + } catch (Throwable throwable) { + logger.log(Level.FINEST, "Couldn't connect to '" + formatHostString(connectionParams.hostName) + + ":" + connectionParams.getPortNumber() + "':\n" + throwable.getMessage(), throwable); + errorMessage = "Couldn't connect to '" + formatHostString(connectionParams.hostName) + + ":" + connectionParams.getPortNumber() + "':\n" + throwable.getMessage(); + } + if (errorMessage != null) { + presenter.showConnectionErrorDialog(errorMessage); + } + presenter.clearMessage(); + presenter.connectionFailed(); + } + } + + @Override + public void setConnectionParams(ConnectionParams connectionParams) { + this.connectionParams = connectionParams; + } + + @Override + public void setPresenter(Presenter presenter) { + this.presenter = (ConnectionPresenter) presenter; + } + + @Override + public void setHasSshSupport(boolean hasSshSupport) { + this.hasSshSupport = hasSshSupport; + } + + @Override + public boolean cancel() { + if (hasSshSupport && sshConnectionManager != null && sshConnectionManager.isConnected()) { + sshConnectionManager.closeConnection(); + sshConnectionManager = null; + } + return super.cancel(true); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingRfbConnectionWorker.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingRfbConnectionWorker.java new file mode 100644 index 0000000..6a94409 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingRfbConnectionWorker.java @@ -0,0 +1,313 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.exceptions.AuthenticationFailedException; +import com.glavsoft.exceptions.FatalException; +import com.glavsoft.exceptions.TransportException; +import com.glavsoft.exceptions.UnsupportedProtocolVersionException; +import com.glavsoft.exceptions.UnsupportedSecurityTypeException; +import com.glavsoft.rfb.IRequestString; +import com.glavsoft.rfb.IRfbSessionListener; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.transport.BaudrateMeter; +import com.glavsoft.transport.Transport; +import com.glavsoft.utils.Strings; +import com.glavsoft.utils.ViewerControlApi; +import com.glavsoft.viewer.settings.UiSettings; +import com.glavsoft.viewer.swing.gui.RequestSomethingDialog; +import com.glavsoft.viewer.workers.ConnectionWorker; +import com.glavsoft.viewer.workers.RfbConnectionWorker; + +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import java.awt.Component; +import java.io.EOFException; +import java.io.IOException; +import java.net.Socket; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +/** +* @author dime at tightvnc.com +*/ +public class SwingRfbConnectionWorker extends SwingWorker implements RfbConnectionWorker, IRfbSessionListener { + + private String predefinedPassword; + private ConnectionPresenter presenter; + private Component parent; + private SwingViewerWindowFactory viewerWindowFactory; + private Logger logger; + private volatile boolean isStoppingProcess; + private SwingViewerWindow viewerWindow; + private String connectionString; + private Protocol workingProtocol; + private Socket workingSocket; + private ProtocolSettings rfbSettings; + private UiSettings uiSettings; + private ViewerControlApi viewerControlApi; + + @Override + public Void doInBackground() throws Exception { + if (null == workingSocket) throw new ConnectionErrorException("Null socket"); + workingSocket.setTcpNoDelay(true); // disable Nagle algorithm + Transport transport = new Transport(workingSocket); + final BaudrateMeter baudrateMeter = new BaudrateMeter(); + transport.setBaudrateMeter(baudrateMeter); + workingProtocol = new Protocol(transport, + new PasswordChooser(connectionString, parent, this), + rfbSettings); + workingProtocol.setConnectionIdRetriever(new ConnectionIdChooser(parent, this)); + viewerControlApi = new ViewerControlApi(workingProtocol, baudrateMeter); + String message = "Handshaking with remote host"; + logger.info(message); + publish(message); + + workingProtocol.handshake(); + return null; + } + + public SwingRfbConnectionWorker(String predefinedPassword, ConnectionPresenter presenter, Component parent, + SwingViewerWindowFactory viewerWindowFactory) { + this.predefinedPassword = predefinedPassword; + this.presenter = presenter; + this.parent = parent; + this.viewerWindowFactory = viewerWindowFactory; + logger = Logger.getLogger(getClass().getName()); + } + + + @Override + protected void process(List strings) { // EDT + String message = strings.get(strings.size() - 1); // get last + presenter.showMessage(message); + } + + @Override + protected void done() { // EDT + try { + get(); + presenter.showMessage("Handshake established"); + ClipboardControllerImpl clipboardController = + new ClipboardControllerImpl(workingProtocol, rfbSettings.getRemoteCharsetName()); + clipboardController.setEnabled(rfbSettings.isAllowClipboardTransfer()); + rfbSettings.addListener(clipboardController); + viewerWindow = viewerWindowFactory.createViewerWindow( + workingProtocol, rfbSettings, uiSettings, connectionString, presenter); + + workingProtocol.startNormalHandling(this, viewerWindow.getRepaintController(), clipboardController); + presenter.showMessage("Started"); + + presenter.successfulRfbConnection(); + } catch (CancellationException e) { + logger.info("Cancelled"); + presenter.showMessage("Cancelled"); + presenter.connectionCancelled(); + } catch (InterruptedException e) { + logger.info("Interrupted"); + presenter.showMessage("Interrupted"); + presenter.connectionFailed(); + } catch (ExecutionException ee) { + String errorTitle; + String errorMessage; + try { + throw ee.getCause(); + } catch (UnsupportedProtocolVersionException e) { + errorTitle = "Unsupported Protocol Version"; + errorMessage = e.getMessage(); + logger.severe(errorTitle + ": " + errorMessage); + } catch (UnsupportedSecurityTypeException e) { + errorTitle = "Unsupported Security Type"; + errorMessage = e.getMessage(); + logger.severe(errorTitle + ": " + errorMessage); + } catch (AuthenticationFailedException e) { + errorTitle = "Authentication Failed"; + errorMessage = e.getMessage(); + logger.severe(errorTitle + ": " + errorMessage); + presenter.clearPredefinedPassword(); + } catch (TransportException e) { + errorTitle = "Connection Error"; + final Throwable cause = e.getCause(); + errorMessage = errorTitle + " : " + e.getMessage(); + if (cause != null) { + if (cause instanceof EOFException) + errorMessage += ", possible reason: remote host not responding."; + logger.throwing("", "", cause); + } + logger.severe(errorMessage); + } catch (EOFException e) { + errorTitle = "Connection Error"; + errorMessage = errorTitle + ": " + e.getMessage(); + logger.severe(errorMessage); + } catch (IOException e) { + errorTitle = "Connection Error"; + errorMessage = errorTitle + ": " + e.getMessage(); + logger.severe(errorMessage); + } catch (FatalException e) { + errorTitle = "Connection Error"; + errorMessage = errorTitle + ": " + e.getMessage(); + logger.severe(errorMessage); + } catch (Throwable e) { + errorTitle = "Error"; + errorMessage = errorTitle + ": " + e.getMessage(); + logger.severe(errorMessage); + } + presenter.showReconnectDialog(errorTitle, errorMessage); + presenter.clearMessage(); + presenter.connectionFailed(); + } + } + + @Override + public void rfbSessionStopped(final String reason) { + if (workingProtocol != null) { + workingProtocol.cleanUpSession(); + } + if (isStoppingProcess) return; + cleanUpUISessionAndConnection(); + logger.info("Rfb session stopped: " + reason); + if (presenter.needReconnection()) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + presenter.showReconnectDialog("Connection error", reason); + presenter.reconnect(predefinedPassword); + } + }); + } + } + + @Override + public boolean cancel() { + boolean res = super.cancel(true); + if (res && workingProtocol != null) { + workingProtocol.cleanUpSession(); + } + cleanUpUISessionAndConnection(); + return res; + } + + private void cleanUpUISessionAndConnection() { + synchronized (this) { + isStoppingProcess = true; + } + if (workingSocket != null && workingSocket.isConnected()) { + try { + workingSocket.close(); + } catch (IOException e) { /*nop*/ } + } + if (viewerWindow != null) { + viewerWindow.close(); + } + synchronized (this) { + isStoppingProcess = false; + } + } + + @Override + public void setWorkingSocket(Socket workingSocket) { + this.workingSocket = workingSocket; + } + + @Override + public void setRfbSettings(ProtocolSettings rfbSettings) { + this.rfbSettings = rfbSettings; + } + + @Override + public void setUiSettings(UiSettings uiSettings) { + this.uiSettings = uiSettings; + } + + @Override + public void setConnectionString(String connectionString) { + this.connectionString = connectionString; + } + + /** + * Ask user for password if needed + */ + private class PasswordChooser implements IRequestString { + private String connectionString; + private final Component parent; + private final ConnectionWorker onCancel; + + private PasswordChooser(String connectionString, Component parent, ConnectionWorker onCancel) { + this.connectionString = connectionString; + this.parent = parent; + this.onCancel = onCancel; + } + + @Override + public String getResult() { + return Strings.isTrimmedEmpty(predefinedPassword) ? + askPassword() : + predefinedPassword; + } + + private String askPassword() { + RequestSomethingDialog dialog = + new RequestSomethingDialog(parent, "VNC Authentication", true, + "Server '" + connectionString + "' requires VNC authentication", "Password:") + .setOkLabel("Login") + .setInputFieldLength(12); + if (!dialog.askResult()) { + onCancel.cancel(); + } + return dialog.getResult(); + } + } + + @Override + public ViewerControlApi getViewerControlApi() { + return viewerControlApi; + } + + private class ConnectionIdChooser implements IRequestString { + private final Component parent; + private final ConnectionWorker onCancel; + + public ConnectionIdChooser(Component parent, ConnectionWorker onCancel) { + this.parent = parent; + this.onCancel = onCancel; + } + + @Override + public String getResult() { + RequestSomethingDialog dialog = + new RequestSomethingDialog(parent, "TcpDispatcher ConnectionId", false, + "TcpDispatcher requires Connection Id.", + "Please get the Connection Id from you peer by any other communication channel\n(ex. phone call or IM) and insert it into the form field below.", + "Connection Id:") + .setInputFieldLength(18); + if (!dialog.askResult()) { + onCancel.cancel(); + } + return dialog.getResult(); + } + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindow.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindow.java new file mode 100644 index 0000000..3c3e240 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindow.java @@ -0,0 +1,993 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.core.SettingsChangedEvent; +import com.glavsoft.rfb.IChangeSettingsListener; +import com.glavsoft.rfb.IRepaintController; +import com.glavsoft.rfb.client.KeyEventMessage; +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.rfb.protocol.tunnel.TunnelType; +import com.glavsoft.utils.Keymap; +import com.glavsoft.utils.Strings; +import com.glavsoft.viewer.settings.UiSettings; +import com.glavsoft.viewer.swing.gui.OptionsDialog; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLayeredPane; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JToggleButton; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.BevelBorder; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GraphicsConfiguration; +import java.awt.Insets; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class SwingViewerWindow implements IChangeSettingsListener, MouseEnteredListener { + public static final int FS_SCROLLING_ACTIVE_BORDER = 20; + private JToggleButton zoomFitButton; + private JToggleButton zoomFullScreenButton; + private JButton zoomInButton; + private JButton zoomOutButton; + private JButton zoomAsIsButton; + private JScrollPane scroller; + private JFrame frame; + private boolean forceResizable = true; + private ButtonsBar buttonsBar; + private Surface surface; + private boolean isSeparateFrame; + private ViewerEventsListener viewerEventsListener; + private final String appName; + private String connectionString; + private ConnectionPresenter presenter; + private Rectangle oldContainerBounds; + private volatile boolean isFullScreen; + private Border oldScrollerBorder; + private JLayeredPane lpane; + private EmptyButtonsBarMouseAdapter buttonsBarMouseAdapter; + private String remoteDesktopName; + private ProtocolSettings rfbSettings; + private UiSettings uiSettings; + private Protocol workingProtocol; + + private boolean isZoomToFitSelected; + private List kbdButtons; + private Container container; + private static Logger logger = Logger.getLogger(SwingViewerWindow.class.getName()); + + public SwingViewerWindow(Protocol workingProtocol, ProtocolSettings rfbSettings, UiSettings uiSettings, Surface surface, + boolean isSeparateFrame, boolean isApplet, ViewerEventsListener viewerEventsListener, + String appName, String connectionString, + ConnectionPresenter presenter, Container externalContainer) { + this.workingProtocol = workingProtocol; + this.rfbSettings = rfbSettings; + this.uiSettings = uiSettings; + this.surface = surface; + this.isSeparateFrame = isSeparateFrame; + this.viewerEventsListener = viewerEventsListener; + this.appName = appName; + this.connectionString = connectionString; + this.presenter = presenter; + createContainer(surface, externalContainer); + + if (uiSettings.showControls) { + createButtonsPanel(workingProtocol, isSeparateFrame? frame: externalContainer, isApplet); + if (isSeparateFrame) registerResizeListener(frame); + updateZoomButtonsState(); + } + if (uiSettings.isFullScreen()) { + switchOnFullscreenMode(); + } + setSurfaceToHandleKbdFocus(); + } + + private void createContainer(final Surface surface, Container externalContainer) { + lpane = new JLayeredPane() { + @Override + public Dimension getSize() { + return surface.getPreferredSize(); + } + @Override + public Dimension getPreferredSize() { + return surface.getPreferredSize(); + } + }; + lpane.setPreferredSize(surface.getPreferredSize()); + lpane.add(surface, JLayeredPane.DEFAULT_LAYER, 0); + scroller = new JScrollPane(); + scroller.getViewport().setBackground(Color.DARK_GRAY); + scroller.setViewportView(lpane); + + if (isSeparateFrame) { + frame = new JFrame(); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent windowEvent) { + super.windowClosing(windowEvent); + fireCloseApp(); + } + }); + frame.setModalExclusionType(Dialog.ModalExclusionType.APPLICATION_EXCLUDE); + Utils.setApplicationIconsForWindow(frame); + frame.setLayout(new BorderLayout(0, 0)); + frame.add(scroller, BorderLayout.CENTER); + +// frame.pack(); + lpane.setSize(surface.getPreferredSize()); + + internalPack(null); + container = frame; + fireContainerCompleted(); + } else { + if (null == externalContainer) throw new IllegalArgumentException("External Container is null"); // TODO catch it somewhere + externalContainer.setLayout(new BorderLayout(0, 0)); + externalContainer.add(scroller, BorderLayout.CENTER); + container = externalContainer; + fireContainerCompleted(); + } + } + + private void fireContainerCompleted() { + if (viewerEventsListener != null) { + viewerEventsListener.onViewerComponentContainerBuilt(this); + } + } + + public void pack() { + final Dimension oldSize = lpane.getSize(); + lpane.setSize(surface.getPreferredSize()); + if (isSeparateFrame && ! isZoomToFitSelected()) { + internalPack(oldSize); + } + if (buttonsBar != null) { + updateZoomButtonsState(); + } + updateWindowTitle(); + } + + public boolean isZoomToFitSelected() { + return isZoomToFitSelected; + } + + public void setZoomToFitSelected(boolean zoomToFitSelected) { + isZoomToFitSelected = zoomToFitSelected; + } + + public void setRemoteDesktopName(String name) { + remoteDesktopName = name; + updateWindowTitle(); + } + + private void updateWindowTitle() { + if (isSeparateFrame) { + frame.setTitle(remoteDesktopName + " [zoom: " + uiSettings.getScalePercentFormatted() + "%]"); + } + } + + private void internalPack(Dimension outerPanelOldSize) { + final Rectangle workareaRectangle = getWorkareaRectangle(); + if (workareaRectangle.equals(frame.getBounds())) { + forceResizable = true; + } + final boolean isHScrollBar = scroller.getHorizontalScrollBar().isShowing() && ! forceResizable; + final boolean isVScrollBar = scroller.getVerticalScrollBar().isShowing() && ! forceResizable; + + boolean isWidthChangeable = true; + boolean isHeightChangeable = true; + if (outerPanelOldSize != null && surface.oldSize != null) { + isWidthChangeable = forceResizable || + (outerPanelOldSize.width == surface.oldSize.width && ! isHScrollBar); + isHeightChangeable = forceResizable || + (outerPanelOldSize.height == surface.oldSize.height && ! isVScrollBar); + } + forceResizable = false; + frame.validate(); + + final Insets containerInsets = frame.getInsets(); + Dimension preferredSize = frame.getPreferredSize(); + Rectangle preferredRectangle = new Rectangle(frame.getLocation(), preferredSize); + + if (null == outerPanelOldSize && workareaRectangle.contains(preferredRectangle)) { + frame.pack(); + } else { + Dimension minDimension = new Dimension( + containerInsets.left + containerInsets.right, containerInsets.top + containerInsets.bottom); + if (buttonsBar != null && buttonsBar.isVisible) { + minDimension.width += buttonsBar.getWidth(); + minDimension.height += buttonsBar.getHeight(); + } + Dimension dim = new Dimension(preferredSize); + Point location = frame.getLocation(); + if ( ! isWidthChangeable) { + dim.width = frame.getWidth(); + } else { + if (isVScrollBar) dim.width += scroller.getVerticalScrollBar().getWidth(); + if (dim.width < minDimension.width) dim.width = minDimension.width; + + int dx = location.x - workareaRectangle.x; + if (dx < 0) { + dx = 0; + location.x = workareaRectangle.x; + } + int w = workareaRectangle.width - dx; + if (w < dim.width) { + int dw = dim.width - w; + if (dw < dx) { + location.x -= dw; + } else { + dim.width = workareaRectangle.width; + location.x = workareaRectangle.x; + } + } + } + if ( ! isHeightChangeable) { + dim.height = frame.getHeight(); + } else { + + if (isHScrollBar) dim.height += scroller.getHorizontalScrollBar().getHeight(); + if (dim.height < minDimension.height) dim.height = minDimension.height; + + int dy = location.y - workareaRectangle.y; + if (dy < 0) { + dy = 0; + location.y = workareaRectangle.y; + } + int h = workareaRectangle.height - dy; + if (h < dim.height) { + int dh = dim.height - h; + if (dh < dy) { + location.y -= dh; + } else { + dim.height = workareaRectangle.height; + location.y = workareaRectangle.y; + } + } + } + if ( ! location.equals(frame.getLocation())) { + frame.setLocation(location); + } + if ( ! isFullScreen ) { + frame.setSize(dim); + } + } + scroller.revalidate(); + } + + private Rectangle getWorkareaRectangle() { + final GraphicsConfiguration graphicsConfiguration = frame.getGraphicsConfiguration(); + final Rectangle screenBounds = graphicsConfiguration.getBounds(); + final Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration); + + screenBounds.x += screenInsets.left; + screenBounds.y += screenInsets.top; + screenBounds.width -= screenInsets.left + screenInsets.right; + screenBounds.height -= screenInsets.top + screenInsets.bottom; + return screenBounds; + } + + void addZoomButtons() { + buttonsBar.createStrut(); + zoomOutButton = buttonsBar.createButton("zoom-out", "Zoom Out", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomFitButton.setSelected(false); + uiSettings.zoomOut(); + } + }); + zoomInButton = buttonsBar.createButton("zoom-in", "Zoom In", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomFitButton.setSelected(false); + uiSettings.zoomIn(); + } + }); + zoomAsIsButton = buttonsBar.createButton("zoom-100", "Zoom 100%", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomFitButton.setSelected(false); + forceResizable = false; + uiSettings.zoomAsIs(); + } + }); + + zoomFitButton = buttonsBar.createToggleButton("zoom-fit", "Zoom to Fit Window", + new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + setZoomToFitSelected(true); + forceResizable = true; + zoomToFit(); + updateZoomButtonsState(); + } else { + setZoomToFitSelected(false); + } + setSurfaceToHandleKbdFocus(); + } + }); + + zoomFullScreenButton = buttonsBar.createToggleButton("zoom-fullscreen", "Full Screen", + new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + updateZoomButtonsState(); + if (e.getStateChange() == ItemEvent.SELECTED) { + uiSettings.setFullScreen(switchOnFullscreenMode()); + } else { + switchOffFullscreenMode(); + uiSettings.setFullScreen(false); + } + setSurfaceToHandleKbdFocus(); + } + }); + if ( ! isSeparateFrame) { + zoomFullScreenButton.setEnabled(false); + zoomFitButton.setEnabled(false); + } + } + + protected void setSurfaceToHandleKbdFocus() { + if (surface != null && ! surface.requestFocusInWindow()) { + surface.requestFocus(); + } + } + + boolean switchOnFullscreenMode() { + zoomFullScreenButton.setSelected(true); + oldContainerBounds = frame.getBounds(); + buttonsBar.setNoFullScreenGroupVisible(false); + setButtonsBarVisible(false); + forceResizable = true; + frame.dispose(); // ? + frame.setUndecorated(true); + frame.setResizable(false); + frame.setVisible(true); // ? + try { + frame.getGraphicsConfiguration().getDevice().setFullScreenWindow(frame); + isFullScreen = true; + scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + oldScrollerBorder = scroller.getBorder(); + scroller.setBorder(new EmptyBorder(0, 0, 0, 0)); + new FullscreenBorderDetectionThread(frame).start(); + } catch (Exception ex) { + Logger.getLogger(this.getClass().getName()).info("Cannot switch into FullScreen mode: " + ex.getMessage()); + return false; + } + return true; + } + + private void switchOffFullscreenMode() { + if (isFullScreen) { + zoomFullScreenButton.setSelected(false); + isFullScreen = false; + buttonsBar.setNoFullScreenGroupVisible(true); + setButtonsBarVisible(true); + try { + frame.dispose(); + frame.setUndecorated(false); + frame.setResizable(true); + frame.getGraphicsConfiguration().getDevice().setFullScreenWindow(null); + } catch (Exception ignore) { + // nop + } + scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scroller.setBorder(oldScrollerBorder); + this.frame.setBounds(oldContainerBounds); + frame.setVisible(true); + pack(); + } + } + + private void zoomToFit() { + Dimension scrollerSize = scroller.getSize(); + Insets scrollerInsets = scroller.getInsets(); + uiSettings.zoomToFit(scrollerSize.width - scrollerInsets.left - scrollerInsets.right, + scrollerSize.height - scrollerInsets.top - scrollerInsets.bottom + + (isFullScreen ? buttonsBar.getHeight() : 0), + workingProtocol.getFbWidth(), workingProtocol.getFbHeight()); + } + + void registerResizeListener(Container container) { + container.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + if (isZoomToFitSelected()) { + zoomToFit(); + updateZoomButtonsState(); + updateWindowTitle(); + setSurfaceToHandleKbdFocus(); + } + } + }); + } + + void updateZoomButtonsState() { + zoomOutButton.setEnabled(uiSettings.getScalePercent() > UiSettings.MIN_SCALE_PERCENT); + zoomInButton.setEnabled(uiSettings.getScalePercent() < UiSettings.MAX_SCALE_PERCENT); + zoomAsIsButton.setEnabled(uiSettings.getScalePercent() != 100); + } + + public ButtonsBar createButtonsBar() { + buttonsBar = new ButtonsBar(); + return buttonsBar; + } + + public void setButtonsBarVisible(boolean isVisible) { + setButtonsBarVisible(isVisible, frame); + } + + private void setButtonsBarVisible(boolean isVisible, Container container) { + buttonsBar.setVisible(isVisible); + if (isVisible) { + buttonsBar.borderOff(); + container.add(buttonsBar.bar, BorderLayout.NORTH); + container.validate(); + } else { + container.remove(buttonsBar.bar); + buttonsBar.borderOn(); + } + } + + public void setButtonsBarVisibleFS(boolean isVisible) { + if (isVisible) { + if ( ! buttonsBar.isVisible) { + lpane.add(buttonsBar.bar, JLayeredPane.POPUP_LAYER, 0); + final int bbWidth = buttonsBar.bar.getPreferredSize().width; + buttonsBar.bar.setBounds( + scroller.getViewport().getViewPosition().x + (scroller.getWidth() - bbWidth)/2, 0, + bbWidth, buttonsBar.bar.getPreferredSize().height); + + // prevent mouse events to through down to Surface + if (null == buttonsBarMouseAdapter) buttonsBarMouseAdapter = new EmptyButtonsBarMouseAdapter(); + buttonsBar.bar.addMouseListener(buttonsBarMouseAdapter); + } + } else { + buttonsBar.bar.removeMouseListener(buttonsBarMouseAdapter); + lpane.remove(buttonsBar.bar); + lpane.repaint(buttonsBar.bar.getBounds()); + } + buttonsBar.setVisible(isVisible); + lpane.repaint(); + lpane.validate(); + buttonsBar.bar.validate(); + } + + public IRepaintController getRepaintController() { + return surface; + } + + void close() { + if (isSeparateFrame && frame != null) { + frame.setVisible(false); + frame.dispose(); + } + } + + @Override + public void mouseEnteredEvent(MouseEvent mouseEvent) { + setSurfaceToHandleKbdFocus(); + } + + public void addMouseListener(MouseListener mouseListener) { + surface.addMouseListener(mouseListener); + } + + public JFrame getFrame() { + return frame; + } + + public void setVisible() { + container.setVisible(true); + } + public void validate() { + container.validate(); + } + + public static class ButtonsBar { + private static final Insets BUTTONS_MARGIN = new Insets(2, 2, 2, 2); + private JPanel bar; + private boolean isVisible; + private ArrayList noFullScreenGroup = new ArrayList(); + + public ButtonsBar() { + bar = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 1)); + } + + public JButton createButton(String iconId, String tooltipText, ActionListener actionListener) { + JButton button = new JButton(Utils.getButtonIcon(iconId)); + button.setToolTipText(tooltipText); + button.setMargin(BUTTONS_MARGIN); + bar.add(button); + button.addActionListener(actionListener); + return button; + } + + public Component createStrut() { + return bar.add(Box.createHorizontalStrut(10)); + } + + public JToggleButton createToggleButton(String iconId, String tooltipText, ItemListener itemListener) { + JToggleButton button = new JToggleButton(Utils.getButtonIcon(iconId)); + button.setToolTipText(tooltipText); + button.setMargin(BUTTONS_MARGIN); + bar.add(button); + button.addItemListener(itemListener); + return button; + } + + public void setVisible(boolean isVisible) { + this.isVisible = isVisible; + if (isVisible) bar.revalidate(); + } + + public int getWidth() { + return bar.getMinimumSize().width; + } + public int getHeight() { + return bar.getMinimumSize().height; + } + + public void borderOn() { + bar.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); + } + + public void borderOff() { + bar.setBorder(BorderFactory.createEmptyBorder()); + } + + public void addToNoFullScreenGroup(Component component) { + noFullScreenGroup.add(component); + } + + public void setNoFullScreenGroupVisible(boolean isVisible) { + for (Component c : noFullScreenGroup) { + c.setVisible(isVisible); + } + } + } + + private static class EmptyButtonsBarMouseAdapter extends MouseAdapter { + // empty + } + + private class FullscreenBorderDetectionThread extends Thread { + public static final int SHOW_HIDE_BUTTONS_BAR_DELAY_IN_MILLS = 700; + private final JFrame frame; + private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture futureForShow; + private ScheduledFuture futureForHide; + private Point mousePoint, oldMousePoint; + private Point viewPosition; + + public FullscreenBorderDetectionThread(JFrame frame) { + super("FS border detector"); + this.frame = frame; + } + + public void run() { + setPriority(Thread.MIN_PRIORITY); + while(isFullScreen) { + mousePoint = MouseInfo.getPointerInfo().getLocation(); + if (null == oldMousePoint) oldMousePoint = mousePoint; + SwingUtilities.convertPointFromScreen(mousePoint, frame); + viewPosition = scroller.getViewport().getViewPosition(); + processButtonsBarVisibility(); + + boolean needScrolling = processVScroll() || processHScroll(); + oldMousePoint = mousePoint; + if (needScrolling) { + cancelShowExecutor(); + setButtonsBarVisibleFS(false); + makeScrolling(viewPosition); + } + try { + Thread.sleep(100); + } catch (Exception e) { + // nop + } + } + } + + private boolean processHScroll() { + if (mousePoint.x < FS_SCROLLING_ACTIVE_BORDER) { + if (viewPosition.x > 0) { + int delta = FS_SCROLLING_ACTIVE_BORDER - mousePoint.x; + if (mousePoint.y != oldMousePoint.y) delta *= 2; // speedify scrolling on mouse moving + viewPosition.x -= delta; + if (viewPosition.x < 0) viewPosition.x = 0; + return true; + } + } else if (mousePoint.x > (frame.getWidth() - FS_SCROLLING_ACTIVE_BORDER)) { + final Rectangle viewRect = scroller.getViewport().getViewRect(); + final int right = viewRect.width + viewRect.x; + if (right < lpane.getSize().width) { + int delta = FS_SCROLLING_ACTIVE_BORDER - (frame.getWidth() - mousePoint.x); + if (mousePoint.y != oldMousePoint.y) delta *= 2; // speedify scrolling on mouse moving + viewPosition.x += delta; + if (viewPosition.x + viewRect.width > lpane.getSize().width) viewPosition.x = + lpane.getSize().width - viewRect.width; + return true; + } + } + return false; + } + + private boolean processVScroll() { + if (mousePoint.y < FS_SCROLLING_ACTIVE_BORDER) { + if (viewPosition.y > 0) { + int delta = FS_SCROLLING_ACTIVE_BORDER - mousePoint.y; + if (mousePoint.x != oldMousePoint.x) delta *= 2; // speedify scrolling on mouse moving + viewPosition.y -= delta; + if (viewPosition.y < 0) viewPosition.y = 0; + return true; + } + } else if (mousePoint.y > (frame.getHeight() - FS_SCROLLING_ACTIVE_BORDER)) { + final Rectangle viewRect = scroller.getViewport().getViewRect(); + final int bottom = viewRect.height + viewRect.y; + if (bottom < lpane.getSize().height) { + int delta = FS_SCROLLING_ACTIVE_BORDER - (frame.getHeight() - mousePoint.y); + if (mousePoint.x != oldMousePoint.x) delta *= 2; // speedify scrolling on mouse moving + viewPosition.y += delta; + if (viewPosition.y + viewRect.height > lpane.getSize().height) viewPosition.y = + lpane.getSize().height - viewRect.height; + return true; + } + } + return false; + } + + private void processButtonsBarVisibility() { + if (mousePoint.y < 1) { + cancelHideExecutor(); + // show buttons bar after delay + if (! buttonsBar.isVisible && (null == futureForShow || futureForShow.isDone())) { + futureForShow = scheduler.schedule(new Runnable() { + @Override + public void run() { + showButtonsBar(); + } + }, SHOW_HIDE_BUTTONS_BAR_DELAY_IN_MILLS, TimeUnit.MILLISECONDS); + } + } else { + cancelShowExecutor(); + } + if (buttonsBar.isVisible && mousePoint.y <= buttonsBar.getHeight()) { + cancelHideExecutor(); + } + if (buttonsBar.isVisible && mousePoint.y > buttonsBar.getHeight()) { + // hide buttons bar after delay + if (null == futureForHide || futureForHide.isDone()) { + futureForHide = scheduler.schedule(new Runnable() { + @Override + public void run() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + setButtonsBarVisibleFS(false); + SwingViewerWindow.this.frame.validate(); + } + }); + } + }, SHOW_HIDE_BUTTONS_BAR_DELAY_IN_MILLS, TimeUnit.MILLISECONDS); + } + } + } + + private void cancelHideExecutor() { + cancelExecutor(futureForHide); + } + private void cancelShowExecutor() { + cancelExecutor(futureForShow); + } + + private void cancelExecutor(ScheduledFuture future) { + if (future != null && ! future.isDone()) { + future.cancel(true); + } + } + + private void makeScrolling(final Point viewPosition) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + scroller.getViewport().setViewPosition(viewPosition); + final Point mousePosition = surface.getMousePosition(); + if (mousePosition != null) { + final MouseEvent mouseEvent = new MouseEvent(frame, 0, 0, 0, + mousePosition.x, mousePosition.y, 0, false); + for (MouseMotionListener mml : surface.getMouseMotionListeners()) { + mml.mouseMoved(mouseEvent); + } + } + } + }); + } + + private void showButtonsBar() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + setButtonsBarVisibleFS(true); + } + }); + } + } + + protected void createButtonsPanel(final Protocol protocol, Container container, boolean isApplet) { + final SwingViewerWindow.ButtonsBar buttonsBar = createButtonsBar(); + + buttonsBar.addToNoFullScreenGroup( + buttonsBar.createButton("options", "Set Options", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showOptionsDialog(); + setSurfaceToHandleKbdFocus(); + } + })); + + buttonsBar.addToNoFullScreenGroup( + buttonsBar.createButton("info", "Show connection info", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showConnectionInfoMessage(); + setSurfaceToHandleKbdFocus(); + } + })); + + buttonsBar.addToNoFullScreenGroup( + buttonsBar.createStrut()); + + buttonsBar.createButton("refresh", "Refresh screen", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + protocol.sendRefreshMessage(); + setSurfaceToHandleKbdFocus(); + } + }); + + addZoomButtons(); + + kbdButtons = new LinkedList(); + + buttonsBar.createStrut(); + + JButton ctrlAltDelButton = buttonsBar.createButton("ctrl-alt-del", "Send 'Ctrl-Alt-Del'", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + sendCtrlAltDel(protocol); + setSurfaceToHandleKbdFocus(); + } + }); + kbdButtons.add(ctrlAltDelButton); + + JButton winButton = buttonsBar.createButton("win", "Send 'Win' key as 'Ctrl-Esc'", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + sendWinKey(protocol); + setSurfaceToHandleKbdFocus(); + } + }); + kbdButtons.add(winButton); + + JToggleButton ctrlButton = buttonsBar.createToggleButton("ctrl", "Ctrl Lock", + new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + protocol.sendMessage(new KeyEventMessage(Keymap.K_CTRL_LEFT, true)); + } else { + protocol.sendMessage(new KeyEventMessage(Keymap.K_CTRL_LEFT, false)); + } + setSurfaceToHandleKbdFocus(); + } + }); + kbdButtons.add(ctrlButton); + + JToggleButton altButton = buttonsBar.createToggleButton("alt", "Alt Lock", + new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + protocol.sendMessage(new KeyEventMessage(Keymap.K_ALT_LEFT, true)); + } else { + protocol.sendMessage(new KeyEventMessage(Keymap.K_ALT_LEFT, false)); + } + setSurfaceToHandleKbdFocus(); + } + }); + kbdButtons.add(altButton); + + ModifierButtonEventListener modifierButtonListener = new ModifierButtonEventListener(); + modifierButtonListener.addButton(KeyEvent.VK_CONTROL, ctrlButton); + modifierButtonListener.addButton(KeyEvent.VK_ALT, altButton); + surface.addModifierListener(modifierButtonListener); + +// JButton fileTransferButton = new JButton(Utils.getButtonIcon("file-transfer")); +// fileTransferButton.setMargin(buttonsMargin); +// buttonBar.add(fileTransferButton); +// buttonsBar.createStrut(); +// +// final JToggleButton viewOnlyButton = buttonsBar.createToggleButton("viewonly", "View Only", +// new ItemListener() { +// @Override +// public void itemStateChanged(ItemEvent e) { +// if (e.getStateChange() == ItemEvent.SELECTED) { +// rfbSettings.setViewOnly(true); +// rfbSettings.fireListeners(); +// } else { +// rfbSettings.setViewOnly(false); +// rfbSettings.fireListeners(); +// } +// setSurfaceToHandleKbdFocus(); +// } +// }); +// viewOnlyButton.setSelected(rfbSettings.isViewOnly()); +// rfbSettings.addListener(new IChangeSettingsListener() { +// @Override +// public void settingsChanged(SettingsChangedEvent event) { +// if (ProtocolSettings.isRfbSettingsChangedFired(event)) { +// ProtocolSettings settings = (ProtocolSettings) event.getSource(); +// viewOnlyButton.setSelected(settings.isViewOnly()); +// } +// } +// }); +// kbdButtons.add(viewOnlyButton); + + buttonsBar.createStrut(); + + buttonsBar.createButton("close", isApplet ? "Disconnect" : "Close", new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + close(); + presenter.setNeedReconnection(false); + presenter.cancelConnection(); + fireCloseApp(); + } + }).setAlignmentX(JComponent.RIGHT_ALIGNMENT); + + setButtonsBarVisible(true, container); + } + + private void fireCloseApp() { + if (viewerEventsListener != null) { + viewerEventsListener.onViewerComponentClosing(); + } + } + + private void sendCtrlAltDel(Protocol protocol) { + protocol.sendMessage(new KeyEventMessage(Keymap.K_CTRL_LEFT, true)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_ALT_LEFT, true)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_DELETE, true)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_DELETE, false)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_ALT_LEFT, false)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_CTRL_LEFT, false)); + } + + private void sendWinKey(Protocol protocol) { + protocol.sendMessage(new KeyEventMessage(Keymap.K_CTRL_LEFT, true)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_ESCAPE, true)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_ESCAPE, false)); + protocol.sendMessage(new KeyEventMessage(Keymap.K_CTRL_LEFT, false)); + } + + @Override + public void settingsChanged(SettingsChangedEvent e) { + if (ProtocolSettings.isRfbSettingsChangedFired(e)) { + ProtocolSettings settings = (ProtocolSettings) e.getSource(); + setEnabledKbdButtons( ! settings.isViewOnly()); + } + } + + private void setEnabledKbdButtons(boolean enabled) { + if (kbdButtons != null) { + for (JComponent b : kbdButtons) { + b.setEnabled(enabled); + } + } + } + + private void showOptionsDialog() { + OptionsDialog optionsDialog = new OptionsDialog(frame); + optionsDialog.initControlsFromSettings(rfbSettings, uiSettings, false); + optionsDialog.setVisible(true); + presenter.saveHistory(); + } + + private void showConnectionInfoMessage() { + StringBuilder message = new StringBuilder(); + if ( ! Strings.isTrimmedEmpty(appName)) { + message.append(appName).append("\n\n"); + } + message.append("Connected to: ").append(remoteDesktopName).append("\n"); + message.append("Host: ").append(connectionString).append("\n\n"); + + message.append("Desktop geometry: ") + .append(String.valueOf(surface.getWidth())) + .append(" \u00D7 ") // multiplication sign + .append(String.valueOf(surface.getHeight())).append("\n"); + message.append("Color format: ") + .append(String.valueOf(Math.round(Math.pow(2, workingProtocol.getPixelFormat().depth)))) + .append(" colors (") + .append(String.valueOf(workingProtocol.getPixelFormat().depth)) + .append(" bits)\n"); + message.append("Current protocol version: ") + .append(workingProtocol.getProtocolVersion()); + if (workingProtocol.isTight()) { + message.append(" tight"); + if (workingProtocol.getTunnelType() != null && workingProtocol.getTunnelType() != TunnelType.NOTUNNEL) { + message.append(" using ").append(workingProtocol.getTunnelType().hrName).append(" tunneling"); + } + } + message.append("\n"); + + JOptionPane infoPane = new JOptionPane(message.toString(), JOptionPane.INFORMATION_MESSAGE); + final JDialog infoDialog = infoPane.createDialog(frame, "VNC connection info"); + infoDialog.setModalityType(Dialog.ModalityType.MODELESS); + infoDialog.setVisible(true); + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindowFactory.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindowFactory.java new file mode 100644 index 0000000..eb9697a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/SwingViewerWindowFactory.java @@ -0,0 +1,118 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.rfb.protocol.Protocol; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.viewer.settings.UiSettings; + +import java.awt.Container; + +/** + * Factory that creates SwingViewerWindow with a number of params + * + * @author dime at tightvnc.com + */ +public class SwingViewerWindowFactory { + + private final boolean isSeparateFrame; + private boolean isApplet = false; + private final ViewerEventsListener viewerEventsListener; + private Container externalContainer; + private String appName; + + /** + * Construct factory for creating SwingViewerWindow at separate frame + * + * @param viewerEventsListener the listener for closing app events + */ + public SwingViewerWindowFactory(ViewerEventsListener viewerEventsListener) { + this(true, viewerEventsListener); + } + + /** + * Construct factory for creating SwingViewerWindow + * + * @param isSeparateFrame if true, creates at the separate frame, else use external container + * @see #setExternalContainer(Container) and put viewer window into the container + * + * @param viewerEventsListener the listener for closing app events + */ + public SwingViewerWindowFactory(boolean isSeparateFrame, + ViewerEventsListener viewerEventsListener) { + this.isSeparateFrame = isSeparateFrame; + this.viewerEventsListener = viewerEventsListener; + } + + /** + * Creates SwingViewerWindow + * @see SwingViewerWindow + * + * @param workingProtocol Protocol object, represents network session that is 'connected' to remote host + * @param rfbSettings rfb protocol settings currently used at the session + * @param uiSettings gui settings currently used to for displaying viewer window + * @param connectionString used to show window title string for displaying remote host name + * @param presenter ConnectionPresenter that response for reconnection and connection history manipulation + * @return the SwingViewerWindow + */ + public SwingViewerWindow createViewerWindow(Protocol workingProtocol, + ProtocolSettings rfbSettings, UiSettings uiSettings, + String connectionString, ConnectionPresenter presenter) { + // TODO do we need in presenter here? or split to to history handling and reconnection ability + Surface surface = new Surface(workingProtocol, uiSettings.getScaleFactor(), uiSettings.getMouseCursorShape()); + final SwingViewerWindow viewerWindow = new SwingViewerWindow(workingProtocol, rfbSettings, uiSettings, + surface, isSeparateFrame, isApplet, viewerEventsListener, appName, connectionString, presenter, externalContainer); + surface.setViewerWindow(viewerWindow); + viewerWindow.setRemoteDesktopName(workingProtocol.getRemoteDesktopName()); + rfbSettings.addListener(viewerWindow); + uiSettings.addListener(surface); + return viewerWindow; + } + + /** + * Sets external container that will be used to contain viewer window + * + * @param externalContainer + */ + public void setExternalContainer(Container externalContainer) { + this.externalContainer = externalContainer; + } + + /** + * Sets whether the application starts as browser applet + * + * @param isApplet + */ + public void setIsApplet(boolean isApplet) { + this.isApplet = isApplet; + } + + /** + * Sets the application name string to show it at info dialog + * @param appName application name + */ + public void setAppName(String appName) { + this.appName = appName; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Utils.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Utils.java new file mode 100644 index 0000000..409da26 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/Utils.java @@ -0,0 +1,141 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +import com.glavsoft.viewer.settings.LocalMouseCursorShape; +import com.glavsoft.viewer.swing.mac.MacUtils; + +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import java.awt.Cursor; +import java.awt.Dialog; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.image.ImageObserver; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Utils for Swing GUI + */ +public class Utils { + private static List icons = new LinkedList(); + + public static List getApplicationIcons() { + if ( ! icons.isEmpty()) return icons; + for (String icoSize : new String[]{"16x16", "32x32", "48x48", "128x128"}) { + URL resource = Utils.class.getResource("/com/glavsoft/viewer/images/tightvnc-logo-" + icoSize + ".png"); + Image image = resource != null ? + Toolkit.getDefaultToolkit().createImage(resource) : + null; + if (image != null) { + icons.add(image); + } + } + return icons; + } + + public static ImageIcon getButtonIcon(String name) { + URL resource = Utils.class.getResource("/com/glavsoft/viewer/images/button-"+name+".png"); + return resource != null ? new ImageIcon(resource) : null; + } + + private static Map cursorCash = new HashMap(); + public static Cursor getCursor(LocalMouseCursorShape cursorShape) { + Cursor cursor = cursorCash.get(cursorShape); + if (cursor != null) return cursor; + String name = cursorShape.getCursorName(); + URL resource = Utils.class.getResource("/com/glavsoft/viewer/images/cursor-"+name+".png"); + if (resource != null) { + Image image = Toolkit.getDefaultToolkit().getImage(resource); + if (image != null) { + final CountDownLatch done = new CountDownLatch(1); + image.getWidth(new ImageObserver() { + @Override + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + boolean isReady = (infoflags & (ALLBITS | ABORT)) != 0; + if (isReady) { + done.countDown(); + } + return ! isReady; + } + }); + try { + done.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return Cursor.getDefaultCursor(); + } + int w = image.getWidth(null); + int h = image.getHeight(null); + if (w < 0 || h < 0) return Cursor.getDefaultCursor(); + w = (int)((w-0.5) / 2); + h = (int)((h-0.5) / 2); + cursor = Toolkit.getDefaultToolkit().createCustomCursor( + image, new Point(w > 0 ? w: 0, h > 0 ? h : 0), name); + if (cursor != null) cursorCash.put(cursorShape, cursor); + } + } + return cursor != null ? cursor : Cursor.getDefaultCursor(); + } + + public static void decorateDialog(Window dialog) { + try { + dialog.setAlwaysOnTop(true); + } catch (SecurityException e) { + // nop + } + dialog.pack(); + if (dialog instanceof JDialog) { + ((JDialog)dialog).setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + } + dialog.toFront(); + Utils.setApplicationIconsForWindow(dialog); + } + + public static void setApplicationIconsForWindow(Window window) { + List icons = getApplicationIcons(); + if (icons.size() != 0) { + if (!MacUtils.isMac()) { + window.setIconImages(icons); + } + } + } + + public static void centerWindow(Window window) { + Point locationPoint = GraphicsEnvironment.getLocalGraphicsEnvironment().getCenterPoint(); + Rectangle bounds = window.getBounds(); + locationPoint.setLocation(locationPoint.x - bounds.width/2, locationPoint.y - bounds.height/2); + window.setLocation(locationPoint); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ViewerEventsListener.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ViewerEventsListener.java new file mode 100644 index 0000000..55430e3 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ViewerEventsListener.java @@ -0,0 +1,41 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing; + +/** + * @author dime at glavsoft.com + */ +public interface ViewerEventsListener { + + /** + * Invoked when close application event fires. + * Ex. when 'Close' button on Connection dialog pressed, when closes viewer window... + */ + void onViewerComponentClosing(); + + /** + * Invoked when (separate) frame contained remote viewport, scroller etc layers created. + */ + void onViewerComponentContainerBuilt(SwingViewerWindow viewerWindow); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/AutoCompletionComboEditorDocument.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/AutoCompletionComboEditorDocument.java new file mode 100644 index 0000000..0911c4f --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/AutoCompletionComboEditorDocument.java @@ -0,0 +1,136 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; +import javax.swing.text.PlainDocument; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; + +/** + * @author dime at tightvnc.com + * + * Using idea by Thomas Bierhance from http://www.orbital-computer.de/JComboBox/ + */ +public class AutoCompletionComboEditorDocument extends PlainDocument { + + private ComboBoxModel model; + private boolean selecting; + private JComboBox comboBox; + private final boolean hidePopupOnFocusLoss; + private JTextComponent editor; + + public AutoCompletionComboEditorDocument(final JComboBox comboBox) { + this.comboBox = comboBox; + this.model = comboBox.getModel(); + this.editor = (JTextComponent)comboBox.getEditor().getEditorComponent(); + editor.setDocument(this); + comboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!selecting) highlightCompletedText(0); + } + }); + + Object selectedItem = comboBox.getSelectedItem(); + if (selectedItem!=null) { + setText(selectedItem.toString()); + highlightCompletedText(0); + } + hidePopupOnFocusLoss = System.getProperty("java.version").startsWith("1.5"); + editor.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (hidePopupOnFocusLoss) comboBox.setPopupVisible(false); + } + }); + } + + @Override + public void remove(int offs, int len) throws BadLocationException { + if (selecting) return; + super.remove(offs, len); + } + + @Override + public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { + if (selecting) return; + super.insertString(offs, str, a); + Object item = lookupItem(getText(0, getLength())); + if (item != null) { + setSelectedItem(item); + setText(item.toString()); + highlightCompletedText(offs + str.length()); + if (comboBox.isDisplayable()) comboBox.setPopupVisible(true); + } + } + + private void setText(String text) { + try { + super.remove(0, getLength()); + super.insertString(0, text, null); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + + private void setSelectedItem(Object item) { + selecting = true; + model.setSelectedItem(item); + selecting = false; + } + + private void highlightCompletedText(int offs) { + JTextComponent editor = (JTextComponent) comboBox.getEditor().getEditorComponent(); + editor.setSelectionStart(offs); + editor.setSelectionEnd(getLength()); + } + + private Object lookupItem(String pattern) { + Object selectedItem = model.getSelectedItem(); + if (selectedItem != null && startsWithIgnoreCase(selectedItem, pattern)) { + return selectedItem; + } else { + for (int i = 0, n = model.getSize(); i < n; i++) { + Object currentItem = model.getElementAt(i); + if (startsWithIgnoreCase(currentItem, pattern)) { + return currentItem; + } + } + } + return null; + } + + private boolean startsWithIgnoreCase(Object currentItem, String pattern) { + return currentItem.toString().toLowerCase().startsWith(pattern.toLowerCase()); + } + + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionDialogView.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionDialogView.java new file mode 100644 index 0000000..6a9ed18 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionDialogView.java @@ -0,0 +1,497 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.viewer.mvp.View; +import com.glavsoft.viewer.settings.ConnectionParams; +import com.glavsoft.viewer.settings.WrongParameterException; +import com.glavsoft.viewer.swing.ConnectionPresenter; +import com.glavsoft.viewer.swing.Utils; +import com.glavsoft.viewer.swing.ViewerEventsListener; +import com.glavsoft.viewer.swing.ssh.SshConnectionManager; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.LinkedList; + +/** + * Dialog window for connection parameters get from. + */ +@SuppressWarnings("serial") +public class ConnectionDialogView extends JPanel implements View, ConnectionView { + private static final int PADDING = 4; + private static final int COLUMNS_HOST_FIELD = 30; + private static final int COLUMNS_PORT_USER_FIELD = 13; + private static final String CLOSE = "Close"; + private static final String CANCEL = "Cancel"; + private ViewerEventsListener onCloseListener; + private final boolean hasSshSupport; + private final JTextField serverPortField; + private JCheckBox useSshTunnelingCheckbox; + private final JComboBox serverNameCombo; + private JTextField sshUserField; + private JTextField sshHostField; + private JTextField sshPortField; + private JLabel sshUserLabel; + private JLabel sshHostLabel; + private JLabel sshPortLabel; + private JLabel ssUserWarningLabel; + private JButton clearHistoryButton; + private JButton connectButton; + private final JFrame view; + private final ConnectionPresenter presenter; + private final StatusBar statusBar; + private boolean connectionInProgress; + private JButton closeCancelButton; + + public ConnectionDialogView(final ViewerEventsListener onCloseListener, + final ConnectionPresenter presenter) { + this.onCloseListener = onCloseListener; + this.hasSshSupport = SshConnectionManager.checkForSshSupport(); + this.presenter = presenter; + + setLayout(new BorderLayout(0, 0)); + JPanel optionsPane = new JPanel(new GridBagLayout()); + add(optionsPane, BorderLayout.CENTER); + optionsPane.setBorder(new EmptyBorder(PADDING, PADDING, PADDING, PADDING)); + + setLayout(new GridBagLayout()); + + int gridRow = 0; + + serverNameCombo = new JComboBox(); + initConnectionsHistoryCombo(); + serverNameCombo.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + Object item = serverNameCombo.getSelectedItem(); + if (item instanceof ConnectionParams) { + presenter.populateFromHistoryItem((ConnectionParams) item); + } + } + }); + + addFormFieldRow(optionsPane, gridRow, new JLabel("Remote Host:"), serverNameCombo, true); + ++gridRow; + + serverPortField = new JTextField(COLUMNS_PORT_USER_FIELD); + + addFormFieldRow(optionsPane, gridRow, new JLabel("Port:"), serverPortField, false); + ++gridRow; + + if (this.hasSshSupport) { + gridRow = createSshOptions(optionsPane, gridRow); + } + + JPanel buttonPanel = createButtons(); + + GridBagConstraints cButtons = new GridBagConstraints(); + cButtons.gridx = 0; cButtons.gridy = gridRow; + cButtons.weightx = 100; cButtons.weighty = 100; + cButtons.gridwidth = 2; cButtons.gridheight = 1; + optionsPane.add(buttonPanel, cButtons); + + view = new JFrame("New TightVNC Connection"); + view.add(this, BorderLayout.CENTER); + statusBar = new StatusBar(); + view.add(statusBar, BorderLayout.SOUTH); + + view.getRootPane().setDefaultButton(connectButton); + view.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent windowEvent) { + super.windowClosing(windowEvent); + onCloseListener.onViewerComponentClosing(); + } + }); +// view.setResizable(false); + Utils.decorateDialog(view); + Utils.centerWindow(view); + } + + private void initConnectionsHistoryCombo() { + serverNameCombo.setEditable(true); + + new AutoCompletionComboEditorDocument(serverNameCombo); // use autocompletion feature for ComboBox + serverNameCombo.setRenderer(new HostnameComboboxRenderer()); + + ConnectionParams prototypeDisplayValue = new ConnectionParams(); + prototypeDisplayValue.hostName = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + serverNameCombo.setPrototypeDisplayValue(prototypeDisplayValue); + } + + @Override + public void showReconnectDialog(final String title, final String message) { + int val = JOptionPane.showConfirmDialog(view, + message + "\nTry another connection?", + title, + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (JOptionPane.NO_OPTION == val) { + presenter.setNeedReconnection(false); + closeView(); + view.dispose(); + closeApp(); + } else { + // TODO return when allowInteractive, close window otherwise +// forceConnectionDialog = allowInteractive; + } + } + + public void setConnectionInProgress(boolean enable) { + if (enable) { + connectionInProgress = true; + closeCancelButton.setText(CANCEL); + connectButton.setEnabled(false); + } else { + connectionInProgress = false; + closeCancelButton.setText(CLOSE); + connectButton.setEnabled(true); + } + } + + private JPanel createButtons() { + JPanel buttonPanel = new JPanel(); + + closeCancelButton = new JButton(CLOSE); + closeCancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (connectionInProgress) { + presenter.cancelConnection(); + setConnectionInProgress(false); + } else { + closeView(); + closeApp(); + } + } + }); + + connectButton = new JButton("Connect"); + buttonPanel.add(connectButton); + connectButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setMessage(""); + Object item = serverNameCombo.getSelectedItem(); + String hostName = item instanceof ConnectionParams ? + ((ConnectionParams) item).hostName : + (String) item; + try { + setConnectionInProgress(true); + presenter.submitConnection(hostName); + } catch (WrongParameterException wpe) { + if (ConnectionPresenter.PROPERTY_HOST_NAME.equals(wpe.getPropertyName())) { + serverNameCombo.requestFocusInWindow(); + } + if (ConnectionPresenter.PROPERTY_RFB_PORT_NUMBER.equals(wpe.getPropertyName())) { + serverPortField.requestFocusInWindow(); + } + showConnectionErrorDialog(wpe.getMessage()); + setConnectionInProgress(false); + } + } + }); + + JButton optionsButton = new JButton("Options..."); + buttonPanel.add(optionsButton); + optionsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + OptionsDialog od = new OptionsDialog(view); + od.initControlsFromSettings(presenter.getRfbSettings(), presenter.getUiSettings(), true); + od.setVisible(true); + view.toFront(); + } + }); + + clearHistoryButton = new JButton("Clear history"); + clearHistoryButton.setToolTipText("Clear connections history"); + buttonPanel.add(clearHistoryButton); + clearHistoryButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + presenter.clearHistory(); + clearHistoryButton.setEnabled(false); + view.toFront(); + } + }); + buttonPanel.add(closeCancelButton); + return buttonPanel; + } + + private int createSshOptions(JPanel pane, int gridRow) { + GridBagConstraints cUseSshTunnelLabel = new GridBagConstraints(); + cUseSshTunnelLabel.gridx = 0; cUseSshTunnelLabel.gridy = gridRow; + cUseSshTunnelLabel.weightx = 100; cUseSshTunnelLabel.weighty = 100; + cUseSshTunnelLabel.gridwidth = 2; cUseSshTunnelLabel.gridheight = 1; + cUseSshTunnelLabel.anchor = GridBagConstraints.LINE_START; + cUseSshTunnelLabel.ipadx = PADDING; + cUseSshTunnelLabel.ipady = 10; + useSshTunnelingCheckbox = new JCheckBox("Use SSH tunneling"); + pane.add(useSshTunnelingCheckbox, cUseSshTunnelLabel); + ++gridRow; + + sshHostLabel = new JLabel("SSH Server:"); + sshHostField = new JTextField(COLUMNS_HOST_FIELD); + addFormFieldRow(pane, gridRow, sshHostLabel, sshHostField, true); + ++gridRow; + + sshPortLabel = new JLabel("SSH Port:"); + sshPortField = new JTextField(COLUMNS_PORT_USER_FIELD); + addFormFieldRow(pane, gridRow, sshPortLabel, sshPortField, false); + ++gridRow; + + sshUserLabel = new JLabel("SSH User:"); + sshUserField = new JTextField(COLUMNS_PORT_USER_FIELD); + JPanel sshUserFieldPane = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + sshUserFieldPane.add(sshUserField); + ssUserWarningLabel = new JLabel(" (will be asked if not specified)"); + sshUserFieldPane.add(ssUserWarningLabel); + addFormFieldRow(pane, gridRow, sshUserLabel, sshUserFieldPane, false); + ++gridRow; + + useSshTunnelingCheckbox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + final boolean useSsh = e.getStateChange() == ItemEvent.SELECTED; + setUseSsh(useSsh); + presenter.setUseSsh(useSsh); + } + }); + + return gridRow; + } + + private void addFormFieldRow(JPanel pane, int gridRow, JLabel label, JComponent field, boolean fill) { + GridBagConstraints cLabel = new GridBagConstraints(); + cLabel.gridx = 0; cLabel.gridy = gridRow; + cLabel.weightx = 0; + cLabel.weighty = 100; + cLabel.gridwidth = cLabel.gridheight = 1; + cLabel.anchor = GridBagConstraints.LINE_END; + cLabel.ipadx = PADDING; + cLabel.ipady = 10; + pane.add(label, cLabel); + + GridBagConstraints cField = new GridBagConstraints(); + cField.gridx = 1; cField.gridy = gridRow; + cField.weightx = 0; cField.weighty = 100; + cField.gridwidth = cField.gridheight = 1; + cField.anchor = GridBagConstraints.LINE_START; + if (fill) cField.fill = GridBagConstraints.HORIZONTAL; + pane.add(field, cField); + } + + /* + * Implicit View interface + */ + public void setMessage(String message) { + statusBar.setMessage(message); + } + + @SuppressWarnings("UnusedDeclaration") + public void setPortNumber(int portNumber) { + serverPortField.setText(String.valueOf(portNumber)); + } + + @SuppressWarnings("UnusedDeclaration") + public String getPortNumber() { + return serverPortField.getText(); + } + + @SuppressWarnings("UnusedDeclaration") + public void setSshHostName(String sshHostName) { + if (hasSshSupport) { + sshHostField.setText(sshHostName); + } + } + + @SuppressWarnings("UnusedDeclaration") + public String getSshHostName() { + if (hasSshSupport) { + return sshHostField.getText(); + } else { return ""; } + } + + @SuppressWarnings("UnusedDeclaration") + public void setSshPortNumber(int sshPortNumber) { + if (hasSshSupport) { + sshPortField.setText(String.valueOf(sshPortNumber)); + } + } + + @SuppressWarnings("UnusedDeclaration") + public String getSshPortNumber() { + if (hasSshSupport) { + return sshPortField.getText(); + } else { return ""; } + } + + @SuppressWarnings("UnusedDeclaration") + public void setSshUserName(String sshUserName) { + if (hasSshSupport) { + sshUserField.setText(sshUserName); + } + } + + @SuppressWarnings("UnusedDeclaration") + public String getSshUserName() { + if (hasSshSupport) { + return sshUserField.getText(); + } else { return ""; } + } + + @SuppressWarnings("UnusedDeclaration") + public void setUseSsh(boolean useSsh) { + if (hasSshSupport) { + useSshTunnelingCheckbox.setSelected(useSsh); + sshUserLabel.setEnabled(useSsh); + sshUserField.setEnabled(useSsh); + ssUserWarningLabel.setEnabled(useSsh); + sshHostLabel.setEnabled(useSsh); + sshHostField.setEnabled(useSsh); + sshPortLabel.setEnabled(useSsh); + sshPortField.setEnabled(useSsh); + } + } + + @SuppressWarnings("UnusedDeclaration") + public boolean getUseSsh() { + return useSshTunnelingCheckbox.isSelected(); + } + + @SuppressWarnings("UnusedDeclaration") + public void setConnectionsList(LinkedList connections) { + serverNameCombo.removeAllItems(); + for (ConnectionParams cp : connections) { + serverNameCombo.addItem(new ConnectionParams(cp)); + } + serverNameCombo.setPopupVisible(false); + clearHistoryButton.setEnabled(serverNameCombo.getItemCount() > 0); + } + /* + * /Implicit View interface + */ + + @Override + public void showView() { + view.setVisible(true); + view.toFront(); + view.repaint(); + } + + @Override + public void closeView() { + view.setVisible(false); + } + + @Override + public void showConnectionErrorDialog(final String message) { + JOptionPane.showMessageDialog(view, message, "Connection error", JOptionPane.ERROR_MESSAGE); + } + + @Override + public void closeApp() { + if (onCloseListener != null) { + onCloseListener.onViewerComponentClosing(); + } + } + + @Override + public JFrame getFrame() { + return view; + } + +} + +class StatusBar extends JPanel { + + private JLabel messageLabel; + + StatusBar() { + setLayout(new BorderLayout()); + setPreferredSize(new Dimension(10, 23)); + + messageLabel = new JLabel(""); + final Font f = messageLabel.getFont(); + messageLabel.setFont(f.deriveFont(f.getStyle() & ~Font.BOLD)); + add(messageLabel, BorderLayout.CENTER); + + JPanel rightPanel = new JPanel(new BorderLayout()); + rightPanel.setOpaque(false); + + + add(rightPanel, BorderLayout.EAST); + setBorder(new Border() { + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + Color oldColor = g.getColor(); + g.translate(x, y); + g.setColor(c.getBackground().darker()); + g.drawLine(0, 0, width -1, 0); + g.setColor(c.getBackground().brighter()); + g.drawLine(0, 1, width -1, 1); + g.translate(-x, -y); + g.setColor(oldColor); + } + @Override + public Insets getBorderInsets(Component c) { + return new Insets(2, 2, 2, 2); + } + @Override + public boolean isBorderOpaque() { + return false; + } + }); + } + + public void setMessage(String message) { + messageLabel.setText(message); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionInfoView.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionInfoView.java new file mode 100644 index 0000000..f3b9a25 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionInfoView.java @@ -0,0 +1,188 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.viewer.swing.ConnectionPresenter; +import com.glavsoft.viewer.swing.Utils; +import com.glavsoft.viewer.swing.ViewerEventsListener; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +/** + * @author dime at tightvnc.com + */ +public class ConnectionInfoView extends JFrame implements ConnectionView { + + private static final String CANCEL = "Cancel"; + private static final int PAD = 8; + private static final String CLOSE = "Close"; + private ViewerEventsListener onCloseListener; + private final ConnectionPresenter presenter; + private final JLabel messageLabel; + private final JLabel infoLabel; + private final JButton cancelOrCloseButton; + + @SuppressWarnings("UnusedDeclaration") + public ConnectionInfoView(final ViewerEventsListener onCloseListener, + final ConnectionPresenter presenter) { + super("Connection"); + this.onCloseListener = onCloseListener; + this.presenter = presenter; + + JPanel outerPane = new JPanel(new BorderLayout(PAD, PAD)); + outerPane.setBorder(new EmptyBorder(PAD, 2*PAD, 2*PAD, 2*PAD)); + final java.util.List applicationIcons = Utils.getApplicationIcons(); + if ( ! applicationIcons.isEmpty()) { + final JLabel iconLabel = new JLabel( + new ImageIcon(applicationIcons.get(applicationIcons.size()-1).getScaledInstance(64, 64, Image.SCALE_SMOOTH))); + outerPane.add(iconLabel, BorderLayout.WEST); + iconLabel.setBorder(new EmptyBorder(PAD, 2*PAD, PAD, 2*PAD)); + } + JPanel listPane = new JPanel(); + outerPane.add(listPane, BorderLayout.CENTER); + listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS)); + listPane.add(Box.createVerticalStrut(PAD)); + + + infoLabel = new JLabel("Connecting..."); + listPane.add(infoLabel); + listPane.add(Box.createVerticalStrut(PAD)); + + messageLabel = new JLabel(" "); + listPane.add(messageLabel, BorderLayout.CENTER); + listPane.add(Box.createVerticalStrut(2*PAD)); + + cancelOrCloseButton = new JButton(CANCEL); + cancelOrCloseButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ConnectionInfoView.this.dispatchEvent(new WindowEvent( + ConnectionInfoView.this, WindowEvent.WINDOW_CLOSING)); + } + }); + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); + buttonPane.add(Box.createHorizontalGlue()); + buttonPane.add(cancelOrCloseButton); + listPane.add(buttonPane); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent windowEvent) { + super.windowClosing(windowEvent); + presenter.cancelConnection(); + closeView(); + closeApp(); + } + }); + + add(outerPane); + getRootPane().setDefaultButton(cancelOrCloseButton); + setMinimumSize(new Dimension(300, 150)); + Utils.decorateDialog(this); + Utils.centerWindow(this); + } + + @Override + public void showView() { + setVisible(true); + toFront(); + repaint(); + } + + @Override + public void closeView() { + setVisible(false); + } + + @Override + public void showReconnectDialog(String title, String message) { + int val = JOptionPane.showConfirmDialog(this, + message + "\nTry another connection?", + title, + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (JOptionPane.NO_OPTION == val) { + presenter.setNeedReconnection(false); + closeView(); + dispose(); + closeApp(); + } + } + + @Override + public void showConnectionErrorDialog(String message) { + JOptionPane.showMessageDialog(this, message, "Connection error", JOptionPane.ERROR_MESSAGE); + } + + @Override + public void closeApp() { + if (onCloseListener != null) { + onCloseListener.onViewerComponentClosing(); + } + } + + @Override + public JFrame getFrame() { + return this; + } + + /* + * Implicit View interface + */ + @SuppressWarnings("UnusedDeclaration") + public void setMessage(String message) { + if (message.isEmpty()) { + cancelOrCloseButton.setText(CANCEL); + } + if ("Cancelled".equals(message)) { + cancelOrCloseButton.setText(CLOSE); + } + messageLabel.setText(message); + pack(); + } + + @SuppressWarnings("UnusedDeclaration") + public void setHostName(String hostName) { + infoLabel.setText("Connecting to host '" + hostName + "'"); + pack(); + } + /* + * /Implicit View interface + */ + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionView.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionView.java new file mode 100644 index 0000000..e476a36 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionView.java @@ -0,0 +1,41 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.viewer.mvp.View; + +import javax.swing.JFrame; + +/** + * @author dime at tightvnc.com + */ +public interface ConnectionView extends View { + void showReconnectDialog(String title, String message); + + void showConnectionErrorDialog(String message); + + void closeApp(); + + JFrame getFrame(); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionsHistory.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionsHistory.java new file mode 100644 index 0000000..25d651e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/ConnectionsHistory.java @@ -0,0 +1,368 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.utils.Strings; +import com.glavsoft.viewer.mvp.Model; +import com.glavsoft.viewer.settings.ConnectionParams; +import com.glavsoft.viewer.settings.UiSettings; +import com.glavsoft.viewer.settings.UiSettingsData; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +/** + * @author dime at tightvnc.com + */ +public class ConnectionsHistory implements Model { + private static int MAX_ITEMS = 32; + public static final String CONNECTIONS_HISTORY_ROOT_NODE = "com/glavsoft/viewer/connectionsHistory"; + public static final String NODE_HOST_NAME = "hostName"; + public static final String NODE_PORT_NUMBER = "portNumber"; + public static final String NODE_SSH_USER_NAME = "sshUserName"; + public static final String NODE_SSH_HOST_NAME = "sshHostName"; + public static final String NODE_SSH_PORT_NUMBER = "sshPortNumber"; + public static final String NODE_USE_SSH = "useSsh"; + public static final String NODE_PROTOCOL_SETTINGS = "protocolSettings"; + public static final String NODE_UI_SETTINGS = "uiSettings"; + private final Logger logger; + + private Map protocolSettingsMap; + private Map uiSettingsDataMap; + LinkedList connections; + + public ConnectionsHistory() { + logger = Logger.getLogger(getClass().getName()); + init(); + retrieve(); + } + + private void init() { + protocolSettingsMap = new HashMap(); + uiSettingsDataMap = new HashMap(); + connections = new LinkedList(); + } + + private void retrieve() { + Preferences connectionsHistoryNode; + try { + connectionsHistoryNode = getConnectionHistoryNode(); + } catch (SecurityException ace) { + return; + } + try { + final String[] orderNums; + orderNums = connectionsHistoryNode.childrenNames(); + SortedMap conns = new TreeMap(); + HashSet uniques = new HashSet(); + for (String orderNum : orderNums) { + int num = 0; + try { + num = Integer.parseInt(orderNum); + } catch (NumberFormatException skip) { + //nop + } + Preferences node = connectionsHistoryNode.node(orderNum); + String hostName = node.get(NODE_HOST_NAME, null); + if (null == hostName) continue; // skip entries without hostName field + ConnectionParams cp = new ConnectionParams(hostName, node.getInt(NODE_PORT_NUMBER, 0), + node.getBoolean(NODE_USE_SSH, false), node.get(NODE_SSH_HOST_NAME, ""), node.getInt(NODE_SSH_PORT_NUMBER, 0), node.get(NODE_SSH_USER_NAME, "") + ); + if (uniques.contains(cp)) continue; // skip duplicates + uniques.add(cp); + conns.put(num, cp); + logger.finest("deserialialize: " + cp.toPrint()); + retrieveProtocolSettings(node, cp); + retrieveUiSettings(node, cp); + } + int itemsCount = 0; + for (ConnectionParams cp : conns.values()) { + if (itemsCount < MAX_ITEMS) { + connections.add(cp); + } else { + connectionsHistoryNode.node(cp.hostName).removeNode(); + } + ++itemsCount; + } + } catch (BackingStoreException e) { + logger.severe("Cannot retrieve connections history info: " + e.getMessage()); + } + } + + private void retrieveUiSettings(Preferences node, ConnectionParams cp) { + byte[] bytes = node.getByteArray(NODE_UI_SETTINGS, new byte[0]); + if (bytes.length != 0) { + try { + UiSettingsData settings = (UiSettingsData) (new ObjectInputStream( + new ByteArrayInputStream(bytes))).readObject(); + uiSettingsDataMap.put(cp, settings); + logger.finest("deserialialize: " + settings); + } catch (IOException e) { + logger.info("Cannot deserialize UiSettings: " + e.getMessage()); + } catch (ClassNotFoundException e) { + logger.severe("Cannot deserialize UiSettings : " + e.getMessage()); + } + } + } + + private void retrieveProtocolSettings(Preferences node, ConnectionParams cp) { + byte[] bytes = node.getByteArray(NODE_PROTOCOL_SETTINGS, new byte[0]); + if (bytes.length != 0) { + try { + ProtocolSettings settings = (ProtocolSettings) (new ObjectInputStream( + new ByteArrayInputStream(bytes))).readObject(); + protocolSettingsMap.put(cp, settings); + logger.finest("deserialialize: " + settings); + } catch (IOException e) { + logger.info("Cannot deserialize ProtocolSettings: " + e.getMessage()); + } catch (ClassNotFoundException e) { + logger.severe("Cannot deserialize ProtocolSettings : " + e.getMessage()); + } + } + } + + /** + * Implicit Model interface + */ + @SuppressWarnings("UnusedDeclaration") + public LinkedList getConnectionsList() { + return connections; + } + + public ProtocolSettings getProtocolSettings(ConnectionParams cp) { + return protocolSettingsMap.get(cp); + } + + public UiSettingsData getUiSettingsData(ConnectionParams cp) { + return uiSettingsDataMap.get(cp); + } + + public void save() { + try { + cleanStorage(); + Preferences connectionsHistoryNode = getConnectionHistoryNode(); + int num = 0; + for (ConnectionParams cp : connections) { + if (num >= MAX_ITEMS) break; + if ( ! Strings.isTrimmedEmpty(cp.hostName)) { + addNode(cp, connectionsHistoryNode, num++); + } + } + } catch (SecurityException ace) { /*nop*/ } + } + + + public void clear() { + cleanStorage(); + init(); + } + + private void cleanStorage() { + Preferences connectionsHistoryNode = getConnectionHistoryNode(); + try { + for (String host : connectionsHistoryNode.childrenNames()) { + connectionsHistoryNode.node(host).removeNode(); + } + } catch (BackingStoreException e) { + logger.severe("Cannot remove node: " + e.getMessage()); + } + } + + private Preferences getConnectionHistoryNode() { + Preferences root = Preferences.userRoot(); + return root.node(CONNECTIONS_HISTORY_ROOT_NODE); + } + + private void addNode(ConnectionParams connectionParams, Preferences connectionsHistoryNode, int orderNum) { + ProtocolSettings protocolSettings = protocolSettingsMap.get(connectionParams); + UiSettingsData uiSettingsData = uiSettingsDataMap.get(connectionParams); + final Preferences node = connectionsHistoryNode.node(String.valueOf(orderNum)); + serializeConnectionParams(node, connectionParams); + serializeProtocolSettings(node, protocolSettings); + serializeUiSettingsData(node, uiSettingsData); + try { + node.flush(); + } catch (BackingStoreException e) { + logger.severe("Cannot retrieve connections history info: " + e.getMessage()); + } + } + + private void serializeUiSettingsData(Preferences node, UiSettingsData uiSettingsData) { + if (uiSettingsData != null) { + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(uiSettingsData); + node.putByteArray(NODE_UI_SETTINGS, byteArrayOutputStream.toByteArray()); + logger.finest("serialized (" + node.name() + ") " + uiSettingsData); + } catch (IOException e) { + logger.severe("Cannot serialize UiSettings: " + e.getMessage()); + } + } + } + + private void serializeProtocolSettings(Preferences node, ProtocolSettings protocolSettings) { + if (protocolSettings != null) { + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(protocolSettings); + node.putByteArray(NODE_PROTOCOL_SETTINGS, byteArrayOutputStream.toByteArray()); + logger.finest("serialized (" + node.name() + ") " + protocolSettings); + } catch (IOException e) { + logger.severe("Cannot serialize ProtocolSettings: " + e.getMessage()); + } + } + } + + private void serializeConnectionParams(Preferences node, ConnectionParams connectionParams) { + node.put(NODE_HOST_NAME, connectionParams.hostName); + node.putInt(NODE_PORT_NUMBER, connectionParams.getPortNumber()); + if (connectionParams.useSsh()) { + node.putBoolean(NODE_USE_SSH, connectionParams.useSsh()); + node.put(NODE_SSH_USER_NAME, connectionParams.sshUserName != null ? connectionParams.sshUserName: ""); + node.put(NODE_SSH_HOST_NAME, connectionParams.sshHostName != null ? connectionParams.sshHostName: ""); + node.putInt(NODE_SSH_PORT_NUMBER, connectionParams.getSshPortNumber()); + } + logger.finest("serialized (" + node.name() + ") " + connectionParams.toPrint()); + } + + public void reorder(ConnectionParams connectionParams) { + reorder(connectionParams, getProtocolSettings(connectionParams), getUiSettingsData(connectionParams)); + } + + public void reorder(ConnectionParams connectionParams, ProtocolSettings protocolSettings, UiSettings uiSettings) { + reorder(connectionParams, protocolSettings, uiSettings != null? uiSettings.getData() : null); + } + + private void reorder(ConnectionParams connectionParams, ProtocolSettings protocolSettings, UiSettingsData uiSettingsData) { + while (connections.remove(connectionParams)) {/*empty - remove all occurrences (if any)*/} + LinkedList cpList = new LinkedList(); + cpList.addAll(connections); + + connections.clear(); + connections.add(new ConnectionParams(connectionParams)); + connections.addAll(cpList); + storeSettings(connectionParams, protocolSettings, uiSettingsData); + } + + private void storeSettings(ConnectionParams connectionParams, ProtocolSettings protocolSettings, UiSettingsData uiSettingsData) { + if (protocolSettings != null) { + ProtocolSettings savedSettings = protocolSettingsMap.get(connectionParams); + if (savedSettings != null) { + savedSettings.copyDataFrom(protocolSettings); + } else { + protocolSettingsMap.put(new ConnectionParams(connectionParams), new ProtocolSettings(protocolSettings)); + } + } + if (uiSettingsData != null) { + uiSettingsDataMap.put(new ConnectionParams(connectionParams), new UiSettingsData(uiSettingsData)); + } + } + + /** + * Search most suitable connectionParams (cp) from history. + * When history is empty, returns original parameter. + * When original parameter is empty (null or hostName is empty) returns the very first cp from history. + * Then subsequently compare cp from history with original for most fields will match in sequent of + * hostName, portNumber, useSsh, sshHostName, sshPortName, sshPortNumber. + * When any match found it returns. + * When no matches found returns the very first cp from history. + * + * @param orig connectionParams to search + * @return most suitable cp + */ + public ConnectionParams getMostSuitableConnection(ConnectionParams orig) { + ConnectionParams res = connections.isEmpty()? orig: connections.get(0); + if (null == orig || Strings.isTrimmedEmpty(orig.hostName)) return res; + for (ConnectionParams cp : connections) { + if (orig.equals(cp)) return cp; + if (compareTextFields(orig.hostName, res.hostName, cp.hostName)) { + res = cp; + continue; + } + if (orig.hostName.equals(cp.hostName) && + comparePorts(orig.getPortNumber(), res.getPortNumber(), cp.getPortNumber())) { + res = cp; + continue; + } + if (orig.hostName.equals(cp.hostName) && + orig.getPortNumber() == cp.getPortNumber() && + orig.useSsh() == cp.useSsh() && orig.useSsh() != res.useSsh()) { + res = cp; + continue; + } + if (orig.hostName.equals(cp.hostName) && + orig.getPortNumber() == cp.getPortNumber() && + orig.useSsh() && cp.useSsh() && + compareTextFields(orig.sshHostName, res.sshHostName, cp.sshHostName)) { + res = cp; + continue; + } + if (orig.hostName.equals(cp.hostName) && + orig.getPortNumber() == cp.getPortNumber() && + orig.useSsh() && cp.useSsh() && + orig.sshHostName != null && orig.sshHostName.equals(cp.hostName) && + comparePorts(orig.getSshPortNumber(), res.getSshPortNumber(), cp.getSshPortNumber())) { + res = cp; + continue; + } + if (orig.hostName.equals(cp.hostName) && + orig.getPortNumber() == cp.getPortNumber() && + orig.useSsh() && cp.useSsh() && + orig.sshHostName != null && orig.sshHostName.equals(cp.hostName) && + orig.getSshPortNumber() == cp.getSshPortNumber() && + compareTextFields(orig.sshUserName, res.sshUserName, cp.sshUserName)) { + res = cp; + } + } + return res; + } + + private boolean comparePorts(int orig, int res, int test) { + return orig == test && orig != res; + } + + private boolean compareTextFields(String orig, String res, String test) { + return (orig != null && test != null && res != null) && + orig.equals(test) && ! orig.equals(res); + } + + public boolean isEmpty() { + return connections.isEmpty(); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/HostnameComboboxRenderer.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/HostnameComboboxRenderer.java new file mode 100644 index 0000000..d1d123a --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/HostnameComboboxRenderer.java @@ -0,0 +1,60 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.viewer.settings.ConnectionParams; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JList; +import java.awt.Component; +import java.awt.Font; + +/** + * @author dime at tightvnc.com + */ +public class HostnameComboboxRenderer extends DefaultListCellRenderer { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + String stringValue = renderListItem((ConnectionParams)value); + setText(stringValue); + setFont(getFont().deriveFont(Font.PLAIN)); + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + return this; + } + + public String renderListItem(ConnectionParams cp) { + String s = "" +cp.hostName + ":" + cp.getPortNumber(); + if (cp.useSsh()) { + s += " (via com.glavsoft.viewer.swing.ssh://" + cp.sshUserName + "@" + cp.sshHostName + ":" + cp.getSshPortNumber() + ")"; + } + return s + ""; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/OptionsDialog.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/OptionsDialog.java new file mode 100644 index 0000000..bb7e215 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/OptionsDialog.java @@ -0,0 +1,520 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.rfb.encoding.EncodingType; +import com.glavsoft.rfb.protocol.LocalPointer; +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.viewer.settings.LocalMouseCursorShape; +import com.glavsoft.viewer.settings.UiSettings; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSlider; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.util.HashMap; +import java.util.Map; + +/** + * Options dialog + */ +@SuppressWarnings("serial") +public class OptionsDialog extends JDialog { + private JSlider jpegQuality; + private JSlider compressionLevel; + private JCheckBox viewOnlyCheckBox; + private ProtocolSettings settings; + private UiSettings uiSettings; + private JCheckBox sharedSession; + + private RadioButtonSelectedState mouseCursorTrackSelected; + private Map mouseCursorTrackMap; + private JCheckBox useCompressionLevel; + private JCheckBox useJpegQuality; + private JLabel jpegQualityPoorLabel; + private JLabel jpegQualityBestLabel; + private JLabel compressionLevelFastLabel; + private JLabel compressionLevelBestLabel; + private JCheckBox allowCopyRect; + private JComboBox encodings; + private JCheckBox disableClipboardTransfer; + private JComboBox colorDepth; + private RadioButtonSelectedState mouseCursorShapeSelected; + private HashMap mouseCursorShapeMap; + + public OptionsDialog(Window owner) { + super(owner, "Connection Options", ModalityType.DOCUMENT_MODAL); + final WindowAdapter onClose = new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + setVisible(false); + } + }; + addWindowListener(onClose); + + JPanel optionsPane = new JPanel(new GridLayout(0, 2)); + add(optionsPane, BorderLayout.CENTER); + + optionsPane.add(createLeftPane()); + optionsPane.add(createRightPane()); + + addButtons(onClose); + + pack(); + } + + public void initControlsFromSettings(ProtocolSettings settings, UiSettings uiSettings, boolean isOnConnect) { + this.settings = settings; + this.uiSettings = uiSettings; + + viewOnlyCheckBox.setSelected(settings.isViewOnly()); + + int i = 0; boolean isNotSetEncoding = true; + while ( encodings.getItemAt(i) != null) { + EncodingType item = encodings.getItemAt(i).type; + if (item.equals(settings.getPreferredEncoding())) { + encodings.setSelectedIndex(i); + isNotSetEncoding = false; + break; + } + ++i; + } + if (isNotSetEncoding) { + encodings.setSelectedItem(0); + } + + sharedSession.setSelected(settings.isShared()); + sharedSession.setEnabled(isOnConnect); + + mouseCursorTrackMap.get(settings.getMouseCursorTrack()).setSelected(true); + mouseCursorTrackSelected.setSelected(settings.getMouseCursorTrack()); + mouseCursorShapeMap.get(uiSettings.getMouseCursorShape()).setSelected(true); + mouseCursorShapeSelected.setSelected(uiSettings.getMouseCursorShape()); + + int depth = settings.getColorDepth(); + i = 0; boolean isNotSet = true; + while ( colorDepth.getItemAt(i) != null) { + int itemDepth = colorDepth.getItemAt(i).depth; + if (itemDepth == depth) { + colorDepth.setSelectedIndex(i); + isNotSet = false; + break; + } + ++i; + } + if (isNotSet) { + colorDepth.setSelectedItem(0); + } + + useCompressionLevel.setSelected(settings.getCompressionLevel() > 0); + compressionLevel.setValue(Math.abs(settings.getCompressionLevel())); + setCompressionLevelPaneEnable(); + + useJpegQuality.setSelected(settings.getJpegQuality() > 0); + jpegQuality.setValue(Math.abs(settings.getJpegQuality())); + setJpegQualityPaneEnable(); + + allowCopyRect.setSelected(settings.isAllowCopyRect()); + disableClipboardTransfer.setSelected( ! settings.isAllowClipboardTransfer()); +} + + private void setSettingsFromControls() { + settings.setViewOnly(viewOnlyCheckBox.isSelected()); + settings.setPreferredEncoding(((EncodingSelectItem)encodings.getSelectedItem()).type); + + settings.setSharedFlag(sharedSession.isSelected()); + settings.setMouseCursorTrack(mouseCursorTrackSelected.getSelected()); + uiSettings.setMouseCursorShape(mouseCursorShapeSelected.getSelected()); + + settings.setColorDepth(((ColorDepthSelectItem) colorDepth.getSelectedItem()).depth); + + settings.setCompressionLevel(useCompressionLevel.isSelected() ? + compressionLevel.getValue() : + - Math.abs(settings.getCompressionLevel())); + settings.setJpegQuality(useJpegQuality.isSelected() ? + jpegQuality.getValue() : + - Math.abs(settings.getJpegQuality())); + settings.setAllowCopyRect(allowCopyRect.isSelected()); + settings.setAllowClipboardTransfer( ! disableClipboardTransfer.isSelected()); + settings.fireListeners(); + } + + private Component createLeftPane() { + Box box = Box.createVerticalBox(); + box.setAlignmentX(LEFT_ALIGNMENT); + + box.add(createEncodingsPanel()); + + box.add(Box.createVerticalGlue()); + return box; + } + + private Component createRightPane() { + Box box = Box.createVerticalBox(); + box.setAlignmentX(LEFT_ALIGNMENT); + + box.add(createRestrictionsPanel()); + box.add(createMouseCursorPanel()); + box.add(createLocalShapePanel()); + + sharedSession = new JCheckBox("Request shared session"); + box.add(new JPanel(new FlowLayout(FlowLayout.LEFT)).add(sharedSession)); + + box.add(Box.createVerticalGlue()); + return box; + } + + private JPanel createRestrictionsPanel() { + JPanel restrictionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + restrictionsPanel.setBorder( + BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Restrictions")); + + Box restrictionsBox = Box.createVerticalBox(); + restrictionsBox.setAlignmentX(LEFT_ALIGNMENT); + restrictionsPanel.add(restrictionsBox); + viewOnlyCheckBox = new JCheckBox("View only (inputs ignored)"); + viewOnlyCheckBox.setAlignmentX(LEFT_ALIGNMENT); + restrictionsBox.add(viewOnlyCheckBox); + + disableClipboardTransfer = new JCheckBox("Disable clipboard transfer"); + disableClipboardTransfer.setAlignmentX(LEFT_ALIGNMENT); + restrictionsBox.add(disableClipboardTransfer); + + return restrictionsPanel; + } + + private JPanel createEncodingsPanel() { + JPanel encodingsPanel = new JPanel(); + encodingsPanel.setAlignmentX(LEFT_ALIGNMENT); + encodingsPanel.setLayout(new BoxLayout(encodingsPanel, BoxLayout.Y_AXIS)); + encodingsPanel.setBorder( + BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Format and Encodings")); + + JPanel encPane = new JPanel(new FlowLayout(FlowLayout.LEFT)); + encPane.setAlignmentX(LEFT_ALIGNMENT); + encPane.add(new JLabel("Preferred encoding: ")); + + encodings = new JComboBox<>(); + encodings.addItem(new EncodingSelectItem(EncodingType.TIGHT)); + encodings.addItem(new EncodingSelectItem(EncodingType.HEXTILE)); + +// encodings.addItem(new EncodingSelectItem(EncodingType.RRE)); +// encodings.addItem(new EncodingSelectItem(EncodingType.ZLIB)); + + encodings.addItem(new EncodingSelectItem(EncodingType.ZRLE)); + encodings.addItem(new EncodingSelectItem(EncodingType.RAW_ENCODING)); + encPane.add(encodings); + encodingsPanel.add(encPane); + + encodingsPanel.add(createColorDepthPanel()); + + addCompressionLevelPane(encodingsPanel); + addJpegQualityLevelPane(encodingsPanel); + + allowCopyRect = new JCheckBox("Allow CopyRect encoding"); + allowCopyRect.setAlignmentX(LEFT_ALIGNMENT); + encodingsPanel.add(allowCopyRect); + + return encodingsPanel; + } + + private static class EncodingSelectItem { + final EncodingType type; + EncodingSelectItem(EncodingType type) { + this.type = type; + } + @Override + public String toString() { + return type.getName(); + } + } + + private static class ColorDepthSelectItem { + final int depth; + final String title; + ColorDepthSelectItem(int depth, String title) { + this.depth = depth; + this.title = title; + } + @Override + public String toString() { + return title; + } + } + + private JPanel createColorDepthPanel() { + JPanel colorDepthPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + colorDepthPanel.setAlignmentX(LEFT_ALIGNMENT); + colorDepthPanel.add(new JLabel("Color format: ")); + + colorDepth = new JComboBox<>(); + colorDepth.addItem(new ColorDepthSelectItem(ProtocolSettings.COLOR_DEPTH_SERVER_SETTINGS, + "Server's default")); + colorDepth.addItem(new ColorDepthSelectItem(ProtocolSettings.COLOR_DEPTH_24, + "16 777 216 colors")); + colorDepth.addItem(new ColorDepthSelectItem(ProtocolSettings.COLOR_DEPTH_16, + "65 536 colors")); + colorDepth.addItem(new ColorDepthSelectItem(ProtocolSettings.COLOR_DEPTH_8, + "256 colors")); + colorDepth.addItem(new ColorDepthSelectItem(ProtocolSettings.COLOR_DEPTH_6, + "64 colors")); + colorDepth.addItem(new ColorDepthSelectItem(ProtocolSettings.COLOR_DEPTH_3, + "8 colors")); + + colorDepthPanel.add(colorDepth); + colorDepth.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + setJpegQualityPaneEnable(); + } + }); + return colorDepthPanel; + } + + private void addJpegQualityLevelPane(JPanel encodingsPanel) { + useJpegQuality = new JCheckBox("Allow JPEG, set quality level:"); + useJpegQuality.setAlignmentX(LEFT_ALIGNMENT); + encodingsPanel.add(useJpegQuality); + + JPanel jpegQualityPane = new JPanel(); + jpegQualityPane.setAlignmentX(LEFT_ALIGNMENT); + jpegQualityPoorLabel = new JLabel("poor"); + jpegQualityPane.add(jpegQualityPoorLabel); + jpegQuality = new JSlider(1, 9, 9); + jpegQualityPane.add(jpegQuality); + jpegQuality.setPaintTicks(true); + jpegQuality.setMinorTickSpacing(1); + jpegQuality.setMajorTickSpacing(1); + jpegQuality.setPaintLabels(true); + jpegQuality.setSnapToTicks(true); + jpegQuality.setFont( + jpegQuality.getFont().deriveFont((float) 8)); + jpegQualityBestLabel = new JLabel("best"); + jpegQualityPane.add(jpegQualityBestLabel); + encodingsPanel.add(jpegQualityPane); + + jpegQualityPoorLabel.setFont(jpegQualityPoorLabel.getFont().deriveFont((float) 10)); + jpegQualityBestLabel.setFont(jpegQualityBestLabel.getFont().deriveFont((float) 10)); + + useJpegQuality.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setJpegQualityPaneEnable(); + } + }); + } + + private void setJpegQualityPaneEnable() { + if (useJpegQuality != null && colorDepth != null) { + int depth = ((ColorDepthSelectItem)colorDepth.getSelectedItem()).depth; + setEnabled(whetherJpegQualityPaneBeEnabled(depth), useJpegQuality); + setEnabled(useJpegQuality.isSelected() && whetherJpegQualityPaneBeEnabled(depth), + jpegQuality, jpegQualityPoorLabel, jpegQualityBestLabel); + } + } + + private boolean whetherJpegQualityPaneBeEnabled(int depth) { + return ProtocolSettings.COLOR_DEPTH_16 == depth || + ProtocolSettings.COLOR_DEPTH_24 == depth || + ProtocolSettings.COLOR_DEPTH_SERVER_SETTINGS == depth; + } + + private void addCompressionLevelPane(JPanel encodingsPanel) { + useCompressionLevel = new JCheckBox("Custom compression level:"); + useCompressionLevel.setAlignmentX(LEFT_ALIGNMENT); + encodingsPanel.add(useCompressionLevel); + + JPanel compressionLevelPane = new JPanel(); + compressionLevelPane.setAlignmentX(LEFT_ALIGNMENT); + compressionLevelFastLabel = new JLabel("fast"); + compressionLevelPane.add(compressionLevelFastLabel); + compressionLevel = new JSlider(1, 9, 1); + compressionLevelPane.add(compressionLevel); + compressionLevel.setPaintTicks(true); + compressionLevel.setMinorTickSpacing(1); + compressionLevel.setMajorTickSpacing(1); + compressionLevel.setPaintLabels(true); + compressionLevel.setSnapToTicks(true); + compressionLevel.setFont(compressionLevel.getFont().deriveFont((float) 8)); + compressionLevelBestLabel = new JLabel("best"); + compressionLevelPane.add(compressionLevelBestLabel); + encodingsPanel.add(compressionLevelPane); + + compressionLevelFastLabel.setFont(compressionLevelFastLabel.getFont().deriveFont((float) 10)); + compressionLevelBestLabel.setFont(compressionLevelBestLabel.getFont().deriveFont((float) 10)); + + useCompressionLevel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setEnabled(useCompressionLevel.isSelected(), + compressionLevel, compressionLevelFastLabel, compressionLevelBestLabel); + } + }); + setCompressionLevelPaneEnable(); + } + + private void setCompressionLevelPaneEnable() { + setEnabled(useCompressionLevel.isSelected(), + compressionLevel, compressionLevelFastLabel, compressionLevelBestLabel); + } + private void setEnabled(boolean isEnabled, JComponent ... comp) { + for (JComponent c : comp) { + c.setEnabled(isEnabled); + } + } + + private JPanel createLocalShapePanel() { + JPanel localCursorShapePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); +// localCursorShapePanel.setLayout(new BoxLayout(localCursorShapePanel, BoxLayout.Y_AXIS)); + localCursorShapePanel.setBorder( + BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Local cursor shape")); + Box localCursorShapeBox = Box.createVerticalBox(); + localCursorShapePanel.add(localCursorShapeBox); + + ButtonGroup mouseCursorShapeTrackGroup = new ButtonGroup(); + mouseCursorShapeSelected = new RadioButtonSelectedState<>(); + mouseCursorShapeMap = new HashMap<>(); + + addRadioButton("Dot cursor", LocalMouseCursorShape.DOT, + mouseCursorShapeSelected, mouseCursorShapeMap, localCursorShapeBox, + mouseCursorShapeTrackGroup); + + addRadioButton("Small dot cursor", LocalMouseCursorShape.SMALL_DOT, + mouseCursorShapeSelected, mouseCursorShapeMap, localCursorShapeBox, + mouseCursorShapeTrackGroup); + + addRadioButton("System default cursor", LocalMouseCursorShape.SYSTEM_DEFAULT, + mouseCursorShapeSelected, mouseCursorShapeMap, localCursorShapeBox, + mouseCursorShapeTrackGroup); + + addRadioButton("No local cursor", LocalMouseCursorShape.NO_CURSOR, + mouseCursorShapeSelected, mouseCursorShapeMap, localCursorShapeBox, + mouseCursorShapeTrackGroup); + + return localCursorShapePanel; + } + + private JPanel createMouseCursorPanel() { + JPanel mouseCursorPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + mouseCursorPanel.setBorder( + BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), "Mouse Cursor")); + Box mouseCursorBox = Box.createVerticalBox(); + mouseCursorPanel.add(mouseCursorBox); + + ButtonGroup mouseCursorTrackGroup = new ButtonGroup(); + + mouseCursorTrackSelected = new RadioButtonSelectedState<>(); + mouseCursorTrackMap = new HashMap<>(); + + addRadioButton("Track remote cursor locally", LocalPointer.ON, + mouseCursorTrackSelected, mouseCursorTrackMap, mouseCursorBox, + mouseCursorTrackGroup); + addRadioButton("Let remote server deal with mouse cursor", + LocalPointer.OFF, + mouseCursorTrackSelected, mouseCursorTrackMap, mouseCursorBox, + mouseCursorTrackGroup); + addRadioButton("Don't show remote cursor", LocalPointer.HIDE, + mouseCursorTrackSelected, mouseCursorTrackMap, mouseCursorBox, + mouseCursorTrackGroup); + return mouseCursorPanel; + } + + private static class RadioButtonSelectedState { + private T state; + + void setSelected(T state) { + this.state = state; + } + + T getSelected() { + return state; + } + + } + + private JRadioButton addRadioButton(String text, final T state, + final RadioButtonSelectedState selected, + Map state2buttonMap, JComponent component, ButtonGroup group) { + JRadioButton radio = new JRadioButton(text); + radio.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + selected.setSelected(state); + } + }); + component.add(radio); + group.add(radio); + state2buttonMap.put(state, radio); + return radio; + } + + private void addButtons(final WindowListener onClose) { + JPanel buttonPanel = new JPanel(); + JButton loginButton = new JButton("Ok"); + buttonPanel.add(loginButton); + loginButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setSettingsFromControls(); + setVisible(false); + } + }); + + JButton closeButton = new JButton("Cancel"); + buttonPanel.add(closeButton); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + onClose.windowClosing(null); + } + }); + add(buttonPanel, BorderLayout.SOUTH); + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/RequestSomethingDialog.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/RequestSomethingDialog.java new file mode 100644 index 0000000..5b4311c --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/gui/RequestSomethingDialog.java @@ -0,0 +1,191 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.gui; + +import com.glavsoft.viewer.swing.Utils; + +import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Image; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.WindowEvent; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Logger; + +/** + * @author dime at tightvnc.com + */ +public class RequestSomethingDialog extends JDialog { + private static final int DEFAULT_INPUT_FIELD_LENGTH = 20; + private static final String OK = "Ok"; + private static final String CANCEL = "Cancel"; + private static final int PAD = 8; + private String answer = ""; + private Boolean result = false; + private String okLabel; + private String cancelLabel; + private int inputFieldLength = DEFAULT_INPUT_FIELD_LENGTH; + + public RequestSomethingDialog(Component parent, String title, final boolean isPassword, String... messages) { + super((Window) parent, title, ModalityType.DOCUMENT_MODAL); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); +// final WindowAdapter onClose = new WindowAdapter() { +// @Override +// public void windowClosing(WindowEvent e) { +// setVisible(false); +// answer = ""; +// } +// }; +// addWindowListener(onClose); + final JTextField inputField; + if (isPassword) { + inputField = new JPasswordField(inputFieldLength); + } else { + inputField = new JTextField(inputFieldLength); + } + JPanel outerPane = new JPanel(new BorderLayout(PAD, PAD)); + outerPane.setBorder(new EmptyBorder(PAD, 2*PAD, 2*PAD, 2*PAD)); + final java.util.List applicationIcons = Utils.getApplicationIcons(); + if ( ! applicationIcons.isEmpty()) { + final JLabel iconLabel = new JLabel( + new ImageIcon(applicationIcons.get(applicationIcons.size()-1).getScaledInstance(64, 64, Image.SCALE_SMOOTH))); + outerPane.add(iconLabel, BorderLayout.WEST); + iconLabel.setBorder(new EmptyBorder(PAD, 2*PAD, PAD, 2*PAD)); + } + JPanel listPane = new JPanel(); + outerPane.add(listPane, BorderLayout.CENTER); + listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS)); + listPane.add(Box.createVerticalStrut(PAD)); + if (messages.length > 0) { + for (int i = 0; i < (messages.length -1); ++i) { + final JLabel label = new JLabel(messages[i]); + label.setAlignmentX(Component.LEFT_ALIGNMENT); + listPane.add(label); + listPane.add(Box.createVerticalStrut(PAD)); + } + String last = messages[messages.length -1]; + if (last.endsWith(":")) { + JPanel inputPanel = new JPanel(); + inputPanel.setLayout(new BoxLayout(inputPanel, BoxLayout.LINE_AXIS)); + final JLabel label = new JLabel(last); + label.setAlignmentX(Component.LEFT_ALIGNMENT); + inputPanel.add(label); + inputPanel.add(inputField); + inputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + listPane.add(inputPanel); + } else { + inputField.setAlignmentX(Component.LEFT_ALIGNMENT); + listPane.add(inputField); + } + } else { + inputField.setAlignmentX(Component.LEFT_ALIGNMENT); + listPane.add(inputField); + } + listPane.add(Box.createVerticalStrut(2*PAD)); + + if (null == okLabel) okLabel = OK; + if (null == cancelLabel) cancelLabel = CANCEL; + JPanel buttonsPane = new JPanel(); + buttonsPane.setLayout(new BoxLayout(buttonsPane, BoxLayout.LINE_AXIS)); + JButton okButton = new JButton(new AbstractAction(okLabel) { + @Override + public void actionPerformed(ActionEvent e) { + result = true; + answer = isPassword ? new String(((JPasswordField)inputField).getPassword()) : inputField.getText(); + RequestSomethingDialog.this.dispatchEvent(new WindowEvent( + RequestSomethingDialog.this, WindowEvent.WINDOW_CLOSING)); + } + }); + JButton cancelButton = new JButton(new AbstractAction(cancelLabel) { + @Override + public void actionPerformed(ActionEvent e) { + result = false; + answer = ""; + RequestSomethingDialog.this.setVisible(false); + RequestSomethingDialog.this.dispatchEvent(new WindowEvent( + RequestSomethingDialog.this, WindowEvent.WINDOW_CLOSING)); + } + }); + buttonsPane.add(Box.createHorizontalGlue()); + buttonsPane.add(cancelButton); + buttonsPane.add(Box.createHorizontalStrut(PAD)); + buttonsPane.add(okButton); + outerPane.add(buttonsPane, BorderLayout.SOUTH); + + add(outerPane); + getRootPane().setDefaultButton(okButton); + if ( ! inputField.requestFocusInWindow()) inputField.requestFocus(); + pack(); + Utils.decorateDialog(this); + Utils.centerWindow(this); + } + + public RequestSomethingDialog setOkLabel(String okLabel) { + this.okLabel = okLabel; + return this; + } + + public RequestSomethingDialog setCancelLabel(String cancelLabel) { + this.cancelLabel = cancelLabel; + return this; + } + + public RequestSomethingDialog setInputFieldLength(int inputFieldLength) { + this.inputFieldLength = inputFieldLength; + return this; + } + + public boolean askResult() { + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + setVisible(true); + } + }); + } catch (InterruptedException e) { + Logger.getLogger(this.getClass().getName()).severe(e.getMessage()); + } catch (InvocationTargetException e) { + Logger.getLogger(this.getClass().getName()).severe(e.getMessage()); + } + return result; + } + + public String getResult() { + return answer; + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacApplicationWrapper.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacApplicationWrapper.java new file mode 100644 index 0000000..f2ac8a7 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacApplicationWrapper.java @@ -0,0 +1,102 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.mac; + +import com.glavsoft.exceptions.CommonException; +import com.glavsoft.utils.LazyLoaded; + +import java.awt.Image; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * @author dime at tightvnc.com + */ +public class MacApplicationWrapper { + + private final Object applicationInstance; + private static final LazyLoaded> applicationClass = new LazyLoaded>(new LazyLoaded.Loader>() { + @Override + public Class load() throws ClassNotFoundException { + return Class.forName("com.apple.eawt.Application"); + } + }); + private static final LazyLoaded getApplicationMethod = new LazyLoaded(new LazyLoaded.Loader() { + @Override + public Method load() throws NoSuchMethodException { + return applicationClass.get().getMethod("getApplication"); + } + }); + + private static final LazyLoaded setDockIconImageMethod = new LazyLoaded(new LazyLoaded.Loader() { + @Override + public Method load() throws Throwable { + return applicationClass.get().getMethod("setDockIconImage", java.awt.Image.class); + } + }); + private static final LazyLoaded setEnabledAboutMenuMethod = new LazyLoaded(new LazyLoaded.Loader() { + @Override + public Method load() throws Throwable { + return applicationClass.get().getMethod("setEnabledAboutMenu", boolean.class); + } + }); + + private MacApplicationWrapper(Object applicationInstance) { + + this.applicationInstance = applicationInstance; + } + + public static MacApplicationWrapper getApplication() throws CommonException { + try { + return new MacApplicationWrapper(getApplicationMethod.get().invoke(null)); + } catch (IllegalAccessException e) { + throw new CommonException("Cannot invoke com.apple.eawt.Application.getApplication: " + e.getMessage()); + } catch (InvocationTargetException e) { + throw new CommonException("Cannot invoke com.apple.eawt.Application.getApplication: " + e.getMessage()); + } + } + + public void setDockIconImage(Image icon) throws CommonException { + if (null == icon) { + throw new CommonException("Icon null"); + } + try { + setDockIconImageMethod.get().invoke(applicationInstance, icon); + } catch (IllegalAccessException e) { + throw new CommonException("Cannot invoke com.apple.eawt.Application.setDockIconImage: " + e.getMessage()); + } catch (InvocationTargetException e) { + throw new CommonException("Cannot invoke com.apple.eawt.Application.setDockIconImage: " + e.getMessage()); + } + } + + public void setEnabledAboutMenu(boolean enable) throws CommonException { + try { + setEnabledAboutMenuMethod.get().invoke(applicationInstance, enable); + } catch (IllegalAccessException e) { + throw new CommonException("Cannot invoke com.apple.eawt.Application.setEnabledAboutMenu: " + e.getMessage()); + } catch (InvocationTargetException e) { + throw new CommonException("Cannot invoke com.apple.eawt.Application.setEnabledAboutMenu: " + e.getMessage()); + } + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacUtils.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacUtils.java new file mode 100644 index 0000000..f95c877 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/mac/MacUtils.java @@ -0,0 +1,62 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.mac; + +import com.glavsoft.utils.LazyLoaded; + +import java.awt.Image; +import java.awt.Toolkit; +import java.net.URL; + +/** + * @author dime at tightvnc.com + */ +public class MacUtils { + private static LazyLoaded isMac = new LazyLoaded(new LazyLoaded.Loader() { + @Override + public Boolean load() { + try { + Class.forName("com.apple.eawt.Application"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + }); + + public static boolean isMac() { + return isMac.get(); + } + + public static Image getIconImage() { + URL resource = MacUtils.class.getResource("/com/glavsoft/viewer/images/tightvnc-logo-128x128.png"); + return resource != null ? + Toolkit.getDefaultToolkit().createImage(resource) : + null; + } + + public static void setName(String name) { + System.setProperty("com.apple.mrj.application.apple.menu.about.name", name); + } +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/PrefsHelper.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/PrefsHelper.java new file mode 100644 index 0000000..449fda3 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/PrefsHelper.java @@ -0,0 +1,95 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.ssh; + +import java.io.IOException; +import java.util.logging.Logger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + + +public class PrefsHelper { + private static Logger logger = Logger.getLogger(PrefsHelper.class.getName()); + + static void clearNode(Preferences node) { + try { // clear wrong data + logger.finer("Clear wrong data from preferences node " + node.name()); + node.clear(); + node.sync(); + } catch (BackingStoreException e) { + logger.warning("Cannot clear/sync preferences node '"+ node.name() +"': " + e.getMessage()); + } + } + + static void addRecordTo(Preferences node, String key, String record) throws IOException { + String out = getStringFrom(node, key); + if (out.length() > 0 && ! out.endsWith("\n")) { + out += ('\n'); + } + out += record + '\n'; + clearNode(node); + update(node, key, out); + } + + private static void update(Preferences node, String key, String value) { + final int length = value.length(); + if (length <= Preferences.MAX_VALUE_LENGTH) { + node.put(key, value); + } else { + for(int idx = 0, cnt = 1 ; idx < length ; ++cnt) { + if ((length - idx) > Preferences.MAX_VALUE_LENGTH) { + node.put(key + "." + cnt, value.substring(idx, idx + Preferences.MAX_VALUE_LENGTH)); + idx += Preferences.MAX_VALUE_LENGTH; + } else { + node.put(key + "." + cnt, value.substring(idx)); + idx = length; + } + } + } + try { + node.sync(); + } catch (BackingStoreException e) { + logger.warning("Cannot sync preferences node '"+ node.name() +"': " + e.getMessage()); + } + } + + static String getStringFrom(Preferences sshNode, String key) { + StringBuilder out = new StringBuilder(); + try { + final String str = sshNode.get(key, ""); + out.append(str); + for (int cnt = 1; ; ++cnt) { + final String partKey = key + "." + cnt; + String part = sshNode.get(partKey, ""); + if (part.length() > 0) out.append(part); + else break; + } + } catch (Exception r) { + logger.warning("Wrong data at '"+ sshNode.absolutePath() + "#" + key +"' prefs: " + r.getMessage()); + clearNode(sshNode); + } + logger.finer("KnownHosts: \n" + out.toString()); + return out.toString(); + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/RequestYesNoDialog.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/RequestYesNoDialog.java new file mode 100644 index 0000000..f3d1188 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/RequestYesNoDialog.java @@ -0,0 +1,64 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.ssh; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import java.awt.Component; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Logger; + +/** + * @author dime at tightvnc.com + */ +public class RequestYesNoDialog { + private final Component parent; + private final String title; + private final String message; + + public RequestYesNoDialog(Component parent, String title, String message) { + this.parent = parent; + this.message = message; + this.title = title; + } + + public boolean ask() { + final int[] result = new int[1]; + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + result[0] = JOptionPane.showConfirmDialog(parent, message, title, + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + } + }); + } catch (InterruptedException e) { + Logger.getLogger(this.getClass().getName()).severe(e.getMessage()); + } catch (InvocationTargetException e) { + Logger.getLogger(this.getClass().getName()).severe(e.getMessage()); + } + return JOptionPane.YES_OPTION == result[0]; + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/SshConnectionManager.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/SshConnectionManager.java new file mode 100644 index 0000000..f1731f6 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/SshConnectionManager.java @@ -0,0 +1,165 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.ssh; + +import com.glavsoft.utils.Strings; +import com.glavsoft.viewer.settings.ConnectionParams; +import com.glavsoft.viewer.swing.CancelConnectionException; +import com.glavsoft.viewer.swing.CancelConnectionQuietlyException; +import com.glavsoft.viewer.swing.ConnectionErrorException; +import com.glavsoft.viewer.swing.gui.RequestSomethingDialog; + +import javax.swing.JFrame; +import java.awt.Component; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class SshConnectionManager { + + static final String SSH_NODE = "com/glavsoft/viewer/ssh"; + static final String KNOWN_HOSTS = "known_hosts"; // both the key of property node and the name of known hosts file as in openssh agreement + private static final String[] PRIV_KEY_FILE_NAMES = new String[]{"id_rsa", "id_dsa", "identity" }; + static final String OPENSSH_CONFIG_DIR_NAME = System.getProperty("user.home") + File.separator + ".ssh"; + private static final String SSH_CONNECTION_MANAGER_IMPLEMENTATION_CLASS_NAME = "com.glavsoft.viewer.swing.ssh.TrileadSsh2ConnectionManager"; + private static final String SSH_LIB_SOME_CLASS_NAME_FOR_CHECKING = "com.trilead.ssh2.Connection"; + String errorMessage = ""; + Component parent; + protected Logger logger; + + SshConnectionManager(Component parent) { + this.parent = parent; + } + + public static SshConnectionManager createManager(Component parentWindow) throws ConnectionErrorException { +// return new TrileadSsh2ConnectionManager(parent); + Throwable ex; + try { + @SuppressWarnings("unchecked") + final Class managerClass = + (Class) Class.forName(SSH_CONNECTION_MANAGER_IMPLEMENTATION_CLASS_NAME); + final Constructor constructor = managerClass.getConstructor(JFrame.class); + return constructor.newInstance(parentWindow); + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) { + ex = e; + } + Logger.getLogger(SshConnectionManager.class.getName()) + .log(Level.WARNING, "Could not instantiate SshConnectionManager, internal error : " + ex.getMessage(), ex); + throw new ConnectionErrorException("Could not create SSH tunnel: internal error."); + } + + public int connect(ConnectionParams connectionParams) throws CancelConnectionException, ConnectionErrorException { + if (Strings.isTrimmedEmpty(connectionParams.sshUserName)) { + RequestSomethingDialog dialog = new RequestSomethingDialog(parent, + "SSH User Name", false, "Please enter the user name for SSH connection:"); + if (dialog.askResult()) { + connectionParams.sshUserName = dialog.getResult(); + } else { + throw new CancelConnectionQuietlyException("Login interrupted by user"); + } + if (Strings.isTrimmedEmpty(connectionParams.sshUserName)) { + throw new CancelConnectionException("No Ssh User Name entered"); + } + } + initSshEngine(); + addIdentityFiles(); + return makeConnectionAndGetPort(connectionParams); + } + + protected abstract void initSshEngine(); + + protected abstract int makeConnectionAndGetPort(ConnectionParams connectionParams) throws CancelConnectionException, ConnectionErrorException; + + boolean isKeyFileEncrypted(File keyFile) { + FileReader fileReader = null; + BufferedReader reader = null; + try { + fileReader = new FileReader(keyFile); + reader = new BufferedReader(fileReader); + String line = reader.readLine(); + if (line.startsWith("-----BEGIN ENCRYPTED PRIVATE KEY-----")) {//PKCS #8 + return true; + } + while ((line = reader.readLine()).indexOf(':') >= 0) { + if (line.indexOf("ENCRYPTED") >0) { + return true; // Proc-Type: 4,ENCRYPTED + } + } + } catch (IOException e) { + logger.warning("Cannot read key file '" + keyFile.getName() + "': " + e.getMessage()); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignore) { + // nop + } + } + if (fileReader != null) { + try { + fileReader.close(); + } catch (IOException ignore) { + // nop + } + } + } + return false; + } + + private void addIdentityFiles() { + for (String fileName : PRIV_KEY_FILE_NAMES) { + File keyFile = new File(OPENSSH_CONFIG_DIR_NAME, fileName); + if (keyFile.exists() && keyFile.isFile()) { + addIdentityFile(keyFile); + } + } + } + + protected abstract void addIdentityFile(File keyFile); + + public String getErrorMessage() { + return errorMessage; + } + + public static boolean checkForSshSupport() { + try { + Class.forName(SSH_CONNECTION_MANAGER_IMPLEMENTATION_CLASS_NAME); + Class.forName(SSH_LIB_SOME_CLASS_NAME_FOR_CHECKING); + return true; + } catch (ClassNotFoundException e) { + return false; + } catch (NoClassDefFoundError e) { + return false; + } + } + + public abstract boolean isConnected(); + + public abstract void closeConnection(); +} \ No newline at end of file diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/TrileadSsh2ConnectionManager.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/TrileadSsh2ConnectionManager.java new file mode 100644 index 0000000..e0fc423 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/swing/ssh/TrileadSsh2ConnectionManager.java @@ -0,0 +1,313 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.swing.ssh; + +import com.glavsoft.exceptions.AuthenticationFailedException; +import com.glavsoft.utils.Strings; +import com.glavsoft.viewer.settings.ConnectionParams; +import com.glavsoft.viewer.swing.CancelConnectionQuietlyException; +import com.glavsoft.viewer.swing.ConnectionErrorException; +import com.glavsoft.viewer.swing.gui.RequestSomethingDialog; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.InteractiveCallback; +import com.trilead.ssh2.KnownHosts; +import com.trilead.ssh2.LocalPortForwarder; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.channel.LocalAcceptThread; +import com.trilead.ssh2.crypto.Base64; + +import javax.swing.JFrame; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.ServerSocket; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.Preferences; + +/** + * @author dime at tightvnc.com + */ +public class TrileadSsh2ConnectionManager extends SshConnectionManager { + + private Set identityFiles = new HashSet<>(); + private LocalPortForwarder portForwarder; + private boolean connected = false; + private Connection connection; + + public TrileadSsh2ConnectionManager(JFrame parentWindow) { + super(parentWindow); + logger = Logger.getLogger(this.getClass().getName()); + } + + @Override + protected void initSshEngine() { + //nop + } + + @Override + protected int makeConnectionAndGetPort(ConnectionParams connectionParams) throws ConnectionErrorException { + connected = false; + int port = 0; + connection = new Connection(connectionParams.getSshHostName(), connectionParams.getSshPortNumber()); + try { + KnownHosts knownHosts = getKnownHosts(); + final ConnectionInfo connectionInfo = connection.connect(new HostVerifier(knownHosts)); + logger.fine("SSH connection established:" + + "\n clientToServerCryptoAlgorithm: " + connectionInfo.clientToServerCryptoAlgorithm + + "\n clientToServerMACAlgorithm: " + connectionInfo.clientToServerMACAlgorithm + + "\n keyExchangeAlgorithm: " + connectionInfo.keyExchangeAlgorithm + + "\n serverHostKeyAlgorithm: " + connectionInfo.serverHostKeyAlgorithm + + "\n serverToClientCryptoAlgorithm: " + connectionInfo.serverToClientCryptoAlgorithm + + "\n serverToClientMACAlgorithm: " + connectionInfo.serverToClientMACAlgorithm); + + if (!connection.isAuthenticationComplete()) { + tryAuthenticate(connectionParams, connection); + } + if (!connection.isAuthenticationComplete()) { + throw new ConnectionErrorException("No supported authentication methods available."); + } + connection.setTCPNoDelay(true); + portForwarder = connection.createLocalPortForwarder(port, connectionParams.getHostName(), connectionParams.getPortNumber()); + port = getPortNumber(portForwarder); + connected = true; + } catch (CancelConnectionQuietlyException | AuthenticationFailedException e) { + logger.fine(e.getMessage()); + errorMessage = e.getMessage(); + } catch (Throwable e) { + logger.log(Level.SEVERE, "Cannot establish SSH connection: " + e.getMessage(), e); + errorMessage = e.getMessage(); + } finally { + if ( ! connected ) { + closeConnection(); + } + } + if ( ! connected ) { + throw new ConnectionErrorException("Cannot establish SSH connection: " + errorMessage); + } + return port; + } + + public void closeConnection() { + if (portForwarder != null) + try { + portForwarder.close(); + portForwarder = null; + } catch (IOException e) { + logger.warning("There was a problem while closing ssh port forwarder: " + e.getMessage()); + } + if (connection != null) { + connection.close(); + connection = null; + } + logger.fine("Close ssh connection"); + } + + private void tryAuthenticate(ConnectionParams connectionParams, Connection connection) throws Throwable { + final String[] remainingAuthMethods = connection.getRemainingAuthMethods(connectionParams.getSshUserName()); + logger.finer("Supported auth methods: " + Arrays.toString(remainingAuthMethods)); + for (String authMethod : remainingAuthMethods) {// publickey, keyboard-interactive + if ("publickey".equals(authMethod)) { + + for (File keyFile : identityFiles) { + logger.fine("Trying 'publickey' auth with " + keyFile); + String passphrase = null; + String title; + String message; + if (isKeyFileEncrypted(keyFile)) { + if (keyFile.getName().contains("rsa")) { + title = "RSA Authentication"; + message = "Enter RSA private key password:"; + } else if (keyFile.getName().contains("dsa")) { + title = "DSA Authentication"; + message = "Enter DSA private key password:"; + } else { + title = "SSH Authentication"; + message = "Enter private key password:"; + } + passphrase = getPassphrase(title, message); + } + if (connection.authenticateWithPublicKey(connectionParams.getSshUserName(), keyFile, passphrase)) { + logger.info("Authenticated with " + keyFile.getName()); + return ; + } + } + } + if ("keyboard-interactive".equals(authMethod)) { + logger.fine("Trying 'keyboard-interactive' auth"); + try { + if (connection.authenticateWithKeyboardInteractive(connectionParams.getSshUserName(), + new InteractiveInputCallback())) { + return; + } else{ + throw new AuthenticationFailedException("Authentication failed"); + } + } catch (IOException e) { + if (e.getCause() != null && e.getCause().getCause() != null) { // go deeper! + throw e.getCause().getCause(); + } else { + if (e.getCause() != null) { + throw e.getCause(); + } else { + throw e; + } + } + } + } + if ("password".equals(authMethod)) { + logger.fine("Trying 'password' auth"); + if (connection.authenticateWithPassword(connectionParams.getSshUserName(), + getPassphrase("Password Authentication", + "Enter password for " + connectionParams.getSshUserName()))) { + return; + } else { + throw new AuthenticationFailedException("Authentication failed"); + } + } + } + } + + private KnownHosts getKnownHosts() throws IOException { + KnownHosts knownHosts = new KnownHosts(); + Preferences sshNode = Preferences.userRoot().node(SSH_NODE); + try { + // code bellow converts byte array to char array + knownHosts.addHostkeys(PrefsHelper.getStringFrom(sshNode, KNOWN_HOSTS).toCharArray()); + } catch (IOException e) { + PrefsHelper.clearNode(sshNode); + } + File knownHostsFile = new File(OPENSSH_CONFIG_DIR_NAME, KNOWN_HOSTS); + if (knownHostsFile.exists() && knownHostsFile.isFile()) { + knownHosts.addHostkeys(knownHostsFile); + } + return knownHosts; + } + + private String getPassphrase(String title, String message) { + RequestSomethingDialog dialog = new RequestSomethingDialog(parent, title, true, message); + return dialog.askResult() ? dialog.getResult() : ""; + } + + /** get local port by reflection because at this library fields we need have got package local visibility */ + private int getPortNumber(LocalPortForwarder portForwarder) throws IOException { + final Field latField; + try { + latField = portForwarder.getClass().getDeclaredField("lat"); + latField.setAccessible(true); + final LocalAcceptThread lat = (LocalAcceptThread) latField.get(portForwarder); + final Field ssField = lat.getClass().getDeclaredField("ss"); + ssField.setAccessible(true); + return ((ServerSocket) ssField.get(lat)).getLocalPort(); + } catch (NoSuchFieldException | IllegalAccessException e) { + logger.throwing(this.getClass().getName(), "getPortNumber(..)", e); + throw new IOException(e.getMessage()); + } + } + + @Override + protected void addIdentityFile(File keyFile) { + identityFiles.add(keyFile); + } + + @Override + public boolean isConnected() { + return connected; + } + + /** + * @author dime at tightvnc.com + */ + private class InteractiveInputCallback implements InteractiveCallback { + + @Override + public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) throws Exception { + String [] answers = new String[numPrompts]; + for (int i = 0; i < numPrompts; ++i) { + RequestSomethingDialog dialog = new RequestSomethingDialog( + parent, + "Keyboard Interactive Authentication", ! echo[i], + Strings.isTrimmedEmpty(name) ? "SSH Authentication": name, + null == instruction ? "" : instruction, prompt[i]) + .setOkLabel("Login"); + if (dialog.askResult()) { + answers[i] = dialog.getResult(); + } else { + throw new CancelConnectionQuietlyException("Login interrupted by user"); + } + } + return answers; + } + } + + private class HostVerifier implements ServerHostKeyVerifier { + private KnownHosts knownHosts; + + HostVerifier(KnownHosts knownHosts) { + this.knownHosts = knownHosts; + } + + @Override + public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception { + int result = knownHosts.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey); + String message; + switch (result) { + case KnownHosts.HOSTKEY_IS_OK: + return true; + case KnownHosts.HOSTKEY_IS_NEW: + message = "Do you want to accept the hostkey (type " + serverHostKeyAlgorithm + ") from " + hostname + " ?"; + break; + case KnownHosts.HOSTKEY_HAS_CHANGED: + message = "WARNING! Hostkey for " + hostname + " has changed!\nAccept anyway?"; + break; + default: + throw new IllegalStateException(); + } + String hexFingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey); + String bubblebabbleFingerprint = KnownHosts.createBubblebabbleFingerprint(serverHostKeyAlgorithm, + serverHostKey); + message += "\n\nHex Fingerprint: " + hexFingerprint + "\nBubblebabble Fingerprint: " + bubblebabbleFingerprint; + final RequestYesNoDialog yesNoDialog = new RequestYesNoDialog(parent, "SSH: Host Verification", message); + final boolean verified = yesNoDialog.ask(); + + if (verified) { + addHostkeyToStorages(hostname, serverHostKeyAlgorithm, serverHostKey); + } + return verified; + } + + void addHostkeyToStorages(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { + Preferences sshNode = Preferences.userRoot().node(SSH_NODE); + String record = hostname + " " + serverHostKeyAlgorithm + " " + new String(Base64.encode(serverHostKey)); + PrefsHelper.addRecordTo(sshNode, KNOWN_HOSTS, record); + + KnownHosts.addHostkeyToFile(new File(OPENSSH_CONFIG_DIR_NAME, KNOWN_HOSTS), + new String[]{hostname}, serverHostKeyAlgorithm, serverHostKey); + } + } + +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/AbstractConnectionWorkerFactory.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/AbstractConnectionWorkerFactory.java new file mode 100644 index 0000000..8f5f2a5 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/AbstractConnectionWorkerFactory.java @@ -0,0 +1,44 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.workers; + +/** + * Factory that creates NetworkConnectionWorker and RfbConnectionWorker + * @author dime at tightvnc.com + */ +public abstract class AbstractConnectionWorkerFactory { + /** + * Creates NetworkConnectionWorker + * @return NetworkConnectionWorker created + */ + public abstract NetworkConnectionWorker createNetworkConnectionWorker(); + + /** + * Creates RfbConnectionWorker + * @return RfbConnectionWorker created + */ + public abstract RfbConnectionWorker createRfbConnectionWorker(); + + public abstract void setPredefinedPassword(String predefinedPassword); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/ConnectionWorker.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/ConnectionWorker.java new file mode 100644 index 0000000..ae6f842 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/ConnectionWorker.java @@ -0,0 +1,46 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.workers; + +/** + * @author dime at tightvnc.com + */ +public interface ConnectionWorker { + /** + * The same as in {@link javax.swing.SwingWorker} + */ + T doInBackground() throws Exception; + + /** + * The same as in {@link javax.swing.SwingWorker} + */ + void execute(); + + /** + * Should cancel worker. + * + * @return true if cancelled successful, false if not (ex. worker is already cancelled) + */ + boolean cancel(); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/NetworkConnectionWorker.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/NetworkConnectionWorker.java new file mode 100644 index 0000000..204b2e8 --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/NetworkConnectionWorker.java @@ -0,0 +1,41 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.workers; + +import com.glavsoft.viewer.mvp.Presenter; +import com.glavsoft.viewer.settings.ConnectionParams; + +import java.net.Socket; + +/** + * @author dime at tightvnc.com + */ +public interface NetworkConnectionWorker extends ConnectionWorker { + + void setConnectionParams(ConnectionParams connectionParams); + + void setPresenter(Presenter presenter); + + void setHasSshSupport(boolean hasSshSupport); +} diff --git a/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/RfbConnectionWorker.java b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/RfbConnectionWorker.java new file mode 100644 index 0000000..c297f7e --- /dev/null +++ b/plugins/vnc/src/main/java/com/glavsoft/viewer/workers/RfbConnectionWorker.java @@ -0,0 +1,46 @@ +// Copyright (C) 2010 - 2014 GlavSoft LLC. +// All rights reserved. +// +// ----------------------------------------------------------------------- +// This file is part of the TightVNC software. Please visit our Web site: +// +// http://www.tightvnc.com/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// 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. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------- +// +package com.glavsoft.viewer.workers; + +import com.glavsoft.rfb.protocol.ProtocolSettings; +import com.glavsoft.utils.ViewerControlApi; +import com.glavsoft.viewer.settings.UiSettings; + +import java.net.Socket; + +/** + * @author dime at tightvnc.com + */ +public interface RfbConnectionWorker extends ConnectionWorker { + + void setWorkingSocket(Socket workingSocket); + + void setRfbSettings(ProtocolSettings rfbSettings); + + void setUiSettings(UiSettings uiSettings); + + void setConnectionString(String connectionString); + + ViewerControlApi getViewerControlApi(); +} diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/MyIcons.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/MyIcons.kt new file mode 100644 index 0000000..6a3e715 --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/MyIcons.kt @@ -0,0 +1,39 @@ +package app.termora.plugins.vnc + +import app.termora.DynamicIcon + +object MyIcons { + val actualZoom by lazy { + DynamicIcon( + "icons/actualZoom.svg", + "icons/actualZoom_dark.svg", + loader = VNCPlugin::class.java.classLoader + ) + } + + + val zoomIn by lazy { + DynamicIcon( + "icons/zoomIn.svg", + "icons/zoomIn_dark.svg", + loader = VNCPlugin::class.java.classLoader + ) + } + + val zoomOut by lazy { + DynamicIcon( + "icons/zoomOut.svg", + "icons/zoomOut_dark.svg", + loader = VNCPlugin::class.java.classLoader + ) + } + + val ctrlAltDel by lazy { + DynamicIcon( + "icons/ctrlAltDel.svg", + "icons/ctrlAltDel_dark.svg", + loader = VNCPlugin::class.java.classLoader + ) + } + +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCHostOptionsPane.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCHostOptionsPane.kt new file mode 100644 index 0000000..955312d --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCHostOptionsPane.kt @@ -0,0 +1,279 @@ +package app.termora.plugins.vnc + +import app.termora.* +import app.termora.plugin.internal.BasicProxyOption +import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.extras.components.FlatComboBox +import com.formdev.flatlaf.ui.FlatTextBorder +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import java.awt.BorderLayout +import java.awt.Component +import java.awt.KeyboardFocusManager +import java.awt.Window +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import java.awt.event.ItemEvent +import javax.swing.* + +internal open class VNCHostOptionsPane : OptionsPane() { + protected val generalOption = GeneralOption() + protected val proxyOption = BasicProxyOption(authenticationTypes = emptyList()) + protected val owner: Window get() = SwingUtilities.getWindowAncestor(this) + + init { + addOption(generalOption) + addOption(proxyOption) + + } + + + open fun getHost(): Host { + val name = generalOption.nameTextField.text + val protocol = VNCProtocolProvider.PROTOCOL + val host = generalOption.hostTextField.text + val port = (generalOption.portTextField.value ?: 5900) as Int + var authentication = Authentication.Companion.No + var proxy = Proxy.Companion.No + val authenticationType = generalOption.authenticationTypeComboBox.selectedItem as AuthenticationType + + if (authenticationType == AuthenticationType.Password) { + authentication = authentication.copy( + type = authenticationType, + password = String(generalOption.passwordTextField.password) + ) + } + + if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) { + proxy = proxy.copy( + type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType, + host = proxyOption.proxyHostTextField.text, + username = proxyOption.proxyUsernameTextField.text, + password = String(proxyOption.proxyPasswordTextField.password), + port = proxyOption.proxyPortTextField.value as Int, + authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType, + ) + } + + + return Host( + name = name, + protocol = protocol, + host = host, + port = port, + authentication = authentication, + proxy = proxy, + sort = System.currentTimeMillis(), + remark = generalOption.remarkTextArea.text, + ) + } + + fun setHost(host: Host) { + generalOption.portTextField.value = host.port + generalOption.nameTextField.text = host.name + generalOption.hostTextField.text = host.host + generalOption.remarkTextArea.text = host.remark + generalOption.authenticationTypeComboBox.selectedItem = host.authentication.type + if (host.authentication.type == AuthenticationType.Password) { + generalOption.passwordTextField.text = host.authentication.password + } + + proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type + proxyOption.proxyHostTextField.text = host.proxy.host + proxyOption.proxyPasswordTextField.text = host.proxy.password + proxyOption.proxyUsernameTextField.text = host.proxy.username + proxyOption.proxyPortTextField.value = host.proxy.port + proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType + + + } + + fun validateFields(): Boolean { + val host = getHost() + + // general + if (validateField(generalOption.nameTextField) + || validateField(generalOption.hostTextField) + ) { + return false + } + + if (host.authentication.type == AuthenticationType.Password) { + if (validateField(generalOption.passwordTextField)) { + return false + } + } + + // proxy + if (host.proxy.type != ProxyType.No) { + if (validateField(proxyOption.proxyHostTextField) + ) { + return false + } + + if (host.proxy.authenticationType != AuthenticationType.No) { + if (validateField(proxyOption.proxyUsernameTextField) + || validateField(proxyOption.proxyPasswordTextField) + ) { + return false + } + } + } + + return true + } + + /** + * 返回 true 表示有错误 + */ + private fun validateField(textField: JTextField): Boolean { + if (textField.isEnabled && textField.text.isBlank()) { + setOutlineError(textField) + return true + } + return false + } + + private fun setOutlineError(textField: JTextField) { + selectOptionJComponent(textField) + textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR) + textField.requestFocusInWindow() + } + + protected inner class GeneralOption : JPanel(BorderLayout()), Option { + val portTextField = PortSpinner(5900) + val nameTextField = OutlineTextField(128) + val hostTextField = OutlineTextField(255) + val passwordTextField = OutlinePasswordField(255) + val remarkTextArea = FixedLengthTextArea(512) + val authenticationTypeComboBox = FlatComboBox() + + init { + initView() + initEvents() + } + + private fun initView() { + add(getCenterComponent(), BorderLayout.CENTER) + + authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + var text = value?.toString() ?: "" + when (value) { + AuthenticationType.Password -> { + text = "Password" + } + + AuthenticationType.PublicKey -> { + text = "Public Key" + } + + AuthenticationType.KeyboardInteractive -> { + text = "Keyboard Interactive" + } + } + return super.getListCellRendererComponent( + list, + text, + index, + isSelected, + cellHasFocus + ) + } + } + + + authenticationTypeComboBox.addItem(AuthenticationType.No) + authenticationTypeComboBox.addItem(AuthenticationType.Password) + + authenticationTypeComboBox.selectedItem = AuthenticationType.Password + + } + + private fun initEvents() { + + + addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() } + removeComponentListener(this) + } + }) + + authenticationTypeComboBox.addItemListener { + if (it.stateChange == ItemEvent.SELECTED) { + passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password + } + } + } + + + override fun getIcon(isSelected: Boolean): Icon { + return Icons.settings + } + + override fun getTitle(): String { + return I18n.getString("termora.new-host.general") + } + + override fun getJComponent(): JComponent { + return this + } + + private fun getCenterComponent(): JComponent { + val layout = FormLayout( + "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default", + "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" + ) + remarkTextArea.setFocusTraversalKeys( + KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS) + ) + remarkTextArea.setFocusTraversalKeys( + KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS) + ) + + remarkTextArea.rows = 8 + remarkTextArea.lineWrap = true + remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4) + + var rows = 1 + val step = 2 + val panel = FormBuilder.create().layout(layout) + .add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows) + .add(nameTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows) + .add(hostTextField).xy(3, rows) + .add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows) + .add(portTextField).xy(7, rows).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows) + .add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows) + .add(passwordTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows) + .add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() }) + .xyw(3, rows, 5).apply { rows += step } + + .build() + + + return panel + } + + } + + +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCPlugin.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCPlugin.kt new file mode 100644 index 0000000..e93ae34 --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCPlugin.kt @@ -0,0 +1,28 @@ +package app.termora.plugins.vnc + +import app.termora.plugin.Extension +import app.termora.plugin.ExtensionSupport +import app.termora.plugin.PaidPlugin +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProviderExtension + +class VNCPlugin : PaidPlugin { + private val support = ExtensionSupport() + + init { + support.addExtension(ProtocolProviderExtension::class.java) { VNCProtocolProviderExtension.instance } + support.addExtension(ProtocolHostPanelExtension::class.java) { VNCProtocolHostPanelExtension.instance } + } + + override fun getAuthor(): String { + return "TermoraDev" + } + + override fun getName(): String { + return "VNC" + } + + override fun getExtensions(clazz: Class): List { + return support.getExtensions(clazz) + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanel.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanel.kt new file mode 100644 index 0000000..d73c906 --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanel.kt @@ -0,0 +1,36 @@ +package app.termora.plugins.vnc + +import app.termora.Disposer +import app.termora.Host +import app.termora.protocol.ProtocolHostPanel +import java.awt.BorderLayout + +class VNCProtocolHostPanel : ProtocolHostPanel() { + private val pane = VNCHostOptionsPane() + + init { + initView() + initEvents() + } + + + private fun initView() { + add(pane, BorderLayout.CENTER) + Disposer.register(this, pane) + } + + private fun initEvents() {} + + + override fun getHost(): Host { + return pane.getHost() + } + + override fun setHost(host: Host) { + pane.setHost(host) + } + + override fun validateFields(): Boolean { + return pane.validateFields() + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanelExtension.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanelExtension.kt new file mode 100644 index 0000000..7663ceb --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolHostPanelExtension.kt @@ -0,0 +1,22 @@ +package app.termora.plugins.vnc + +import app.termora.account.AccountOwner +import app.termora.protocol.ProtocolHostPanel +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProvider + +internal class VNCProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension { + companion object { + val instance = VNCProtocolHostPanelExtension() + + } + + override fun getProtocolProvider(): ProtocolProvider { + return VNCProtocolProvider.instance + } + + override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel { + return VNCProtocolHostPanel() + } + +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProvider.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProvider.kt new file mode 100644 index 0000000..de78a22 --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProvider.kt @@ -0,0 +1,34 @@ +package app.termora.plugins.vnc + +import app.termora.DynamicIcon +import app.termora.Host +import app.termora.TerminalTab +import app.termora.WindowScope +import app.termora.actions.DataProvider +import app.termora.protocol.GenericProtocolProvider + +internal class VNCProtocolProvider private constructor() : GenericProtocolProvider { + companion object { + val instance = VNCProtocolProvider() + const val PROTOCOL = "VNC" + private val icon = + DynamicIcon( + "META-INF/pluginIcon.svg", + "META-INF/pluginIcon_dark.svg", + loader = VNCPlugin::class.java.classLoader + ) + } + + override fun getProtocol(): String { + return PROTOCOL + } + + override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab { + return VNCTerminalTab(windowScope, host) + } + + override fun getIcon(width: Int, height: Int): DynamicIcon { + return icon + } + +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProviderExtension.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProviderExtension.kt new file mode 100644 index 0000000..252f058 --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCProtocolProviderExtension.kt @@ -0,0 +1,14 @@ +package app.termora.plugins.vnc + +import app.termora.protocol.ProtocolProvider +import app.termora.protocol.ProtocolProviderExtension + +internal class VNCProtocolProviderExtension private constructor() : ProtocolProviderExtension { + companion object { + val instance = VNCProtocolProviderExtension() + } + + override fun getProtocolProvider(): ProtocolProvider { + return VNCProtocolProvider.instance + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCTerminalTab.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCTerminalTab.kt new file mode 100644 index 0000000..70160ea --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCTerminalTab.kt @@ -0,0 +1,28 @@ +package app.termora.plugins.vnc + +import app.termora.Disposer +import app.termora.Host +import app.termora.HostTerminalTab +import app.termora.WindowScope +import javax.swing.Icon +import javax.swing.JComponent + +class VNCTerminalTab(windowScope: WindowScope, host: Host) : HostTerminalTab(windowScope, host) { + private val viewer = VNCViewer(host) + + override fun getJComponent(): JComponent { + return viewer + } + + override fun getIcon(): Icon { + return VNCProtocolProvider.instance.getIcon() + } + + override fun canReconnect(): Boolean { + return false + } + + override fun dispose() { + Disposer.dispose(viewer) + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCViewer.kt b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCViewer.kt new file mode 100644 index 0000000..d67b4ec --- /dev/null +++ b/plugins/vnc/src/main/kotlin/app/termora/plugins/vnc/VNCViewer.kt @@ -0,0 +1,263 @@ +package app.termora.plugins.vnc + +import app.termora.* +import com.glavsoft.rfb.client.KeyEventMessage +import com.glavsoft.rfb.encoding.EncodingType +import com.glavsoft.rfb.protocol.Protocol +import com.glavsoft.rfb.protocol.ProtocolSettings +import com.glavsoft.transport.BaudrateMeter +import com.glavsoft.transport.Transport +import com.glavsoft.utils.Keymap +import com.glavsoft.viewer.settings.LocalMouseCursorShape +import com.glavsoft.viewer.settings.UiSettings +import com.glavsoft.viewer.swing.ClipboardControllerImpl +import com.glavsoft.viewer.swing.Surface +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing +import org.apache.commons.lang3.exception.ExceptionUtils +import java.awt.AWTEvent +import java.awt.BorderLayout +import java.awt.Graphics +import java.awt.event.AWTEventListener +import java.awt.event.ActionEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.net.InetSocketAddress +import java.net.Proxy +import java.net.Socket +import java.util.concurrent.Executors +import javax.swing.* +import kotlin.math.max + + +class VNCViewer(private val host: Host) : JPanel(BorderLayout()), Disposable { + private var socket: Socket? = null + private var surface: Surface? = null + private var protocol: Protocol? = null + private var uiSettings: UiSettings? = null + + private val layeredPane = LayeredPane() + private val toolbar = MyToolbar() + private val scrollPane = JScrollPane() + + private val executorService = Executors.newVirtualThreadPerTaskExecutor() + private val coroutineDispatcher = executorService.asCoroutineDispatcher() + private val coroutineScope = CoroutineScope(coroutineDispatcher) + private val owner get() = SwingUtilities.getWindowAncestor(this) + + init { + initView() + initEvents() + } + + fun initView() { + scrollPane.border = BorderFactory.createEmptyBorder() + + layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER as Any) + layeredPane.add(toolbar, JLayeredPane.PALETTE_LAYER as Any) + add(layeredPane, BorderLayout.CENTER) + } + + fun initEvents() { + coroutineScope.launch { + try { + connect() + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Swing) { + OptionPane.showMessageDialog( + owner, + e.message ?: ExceptionUtils.getMessage(e), + messageType = JOptionPane.ERROR_MESSAGE + ) + } + } + } + + toolkit.addAWTEventListener(toolbar, AWTEvent.MOUSE_MOTION_EVENT_MASK) + } + + private suspend fun connect() { + withContext(Dispatchers.Swing) { + surface?.let { scrollPane.remove(it) } + } + + disconnect() + + val proxy = when (host.proxy.type) { + ProxyType.HTTP -> Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port)) + ProxyType.SOCKS5 -> Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port)) + else -> Proxy.NO_PROXY + } + + val socket = Socket(proxy).also { this.socket = it } + socket.keepAlive = true + socket.connect(InetSocketAddress(host.host, host.port)) + socket.tcpNoDelay = true + + val transport = Transport(socket) + transport.setBaudrateMeter(BaudrateMeter()) + val uiSettings = UiSettings().also { uiSettings = it } + val protocolSettings = ProtocolSettings.getDefaultSettings() + protocolSettings.preferredEncoding = EncodingType.ZRLE + + val protocol = Protocol(transport, { host.authentication.password }, protocolSettings) + .also { this.protocol = it } + val surface = Surface(protocol, 1.0, LocalMouseCursorShape.NO_CURSOR).also { this.surface = it } + + uiSettings.addListener(surface) + protocolSettings.addListener(surface) + + protocol.handshake() + protocol.startNormalHandling({ disconnect() }, surface, ClipboardControllerImpl(protocol, "GBK")) + + withContext(Dispatchers.Swing) { + scrollPane.setViewportView(surface) + } + } + + private fun disconnect() { + socket?.close() + + protocol = null + surface = null + socket = null + uiSettings = null + } + + override fun dispose() { + disconnect() + + toolkit.removeAWTEventListener(toolbar) + + coroutineScope.cancel() + coroutineDispatcher.close() + executorService.shutdownNow() + } + + private inner class MyToolbar : JToolBar(), AWTEventListener { + private val zoomInBtn = JButton(MyIcons.zoomIn) + private val zoomOutBtn = JButton(MyIcons.zoomOut) + private val actualZoomBtn = JButton(MyIcons.actualZoom) + private val fitContentBtn = JButton(Icons.fitContent) + private val ctrlAltDelBtn = JButton(MyIcons.ctrlAltDel) + + var collapse = true + + init { + initView() + initEvents() + } + + fun initView() { + add(zoomInBtn) + add(zoomOutBtn) + add(actualZoomBtn) + add(fitContentBtn) + addSeparator() + add(ctrlAltDelBtn) + + border = BorderFactory.createMatteBorder(0, 1, 1, 1, DynamicColor.BorderColor) + } + + fun initEvents() { + zoomInBtn.addActionListener(object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + uiSettings?.zoomIn() + } + }) + + zoomOutBtn.addActionListener(object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + uiSettings?.zoomOut() + } + }) + + actualZoomBtn.addActionListener(object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + uiSettings?.zoomAsIs() + } + }) + + fitContentBtn.addActionListener(object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + val uiSettings = uiSettings ?: return + val protocol = protocol ?: return + uiSettings.zoomToFit(layeredPane.width, layeredPane.height, protocol.fbWidth, protocol.fbHeight) + } + }) + + ctrlAltDelBtn.addActionListener(object : AbstractAction() { + override fun actionPerformed(e: ActionEvent) { + val protocol = protocol ?: return + + protocol.sendMessage(KeyEventMessage(Keymap.K_CTRL_LEFT, true)) + protocol.sendMessage(KeyEventMessage(Keymap.K_ALT_LEFT, true)) + protocol.sendMessage(KeyEventMessage(Keymap.K_DELETE, true)) + protocol.sendMessage(KeyEventMessage(Keymap.K_DELETE, false)) + protocol.sendMessage(KeyEventMessage(Keymap.K_ALT_LEFT, false)) + protocol.sendMessage(KeyEventMessage(Keymap.K_CTRL_LEFT, false)) + } + }) + + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + println("Enter") + } + + override fun mouseExited(e: MouseEvent) { + println("Exit") + } + }) + + } + + override fun eventDispatched(event: AWTEvent) { + if (event is MouseEvent) { + if (event.id != MouseEvent.MOUSE_MOVED) { + return + } + + val c = event.component ?: return + + if (layeredPane.isShowing.not()) return + + val collapse = SwingUtilities.isDescendingFrom(c, toolbar).not() + + if (this.collapse != collapse) { + this.collapse = collapse + revalidateBounds() + } + } + } + + fun revalidateBounds() { + val h = max(height, preferredSize.height) + val w = max(width, preferredSize.width) + setBounds((layeredPane.width - w) / 2, 0, w, if (collapse) 4 else h) + revalidate() + repaint() + } + + override fun paintChildren(g: Graphics?) { + if (collapse) { + return + } + super.paintChildren(g) + } + } + + private class LayeredPane : JLayeredPane() { + override fun doLayout() { + synchronized(treeLock) { + for (c in components) { + if (c is MyToolbar) { + c.revalidateBounds() + } else { + c.setBounds(0, 0, width, height) + } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/vnc/src/main/resources/META-INF/plugin.xml b/plugins/vnc/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..18e268f --- /dev/null +++ b/plugins/vnc/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,22 @@ + + + vnc + + VNC Viewer + + + + ${projectVersion} + + + + app.termora.plugins.vnc.VNCPlugin + + + VNC Viewer + + + TermoraDev + + + diff --git a/plugins/vnc/src/main/resources/META-INF/pluginIcon.svg b/plugins/vnc/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..7bb4bae --- /dev/null +++ b/plugins/vnc/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/vnc/src/main/resources/META-INF/pluginIcon_dark.svg b/plugins/vnc/src/main/resources/META-INF/pluginIcon_dark.svg new file mode 100644 index 0000000..afd7f07 --- /dev/null +++ b/plugins/vnc/src/main/resources/META-INF/pluginIcon_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-alt.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-alt.png new file mode 100644 index 0000000000000000000000000000000000000000..32897856d72a25b1bad94df628a147af9f19fbea GIT binary patch literal 263 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs({3|&H}j{}8dGeaUuobz*Y zQ}arITm}Z`qSVBa)D(sC%#sWRcTeAd6une-pm?08i(`nz>ARByxfm2VSc9!@SpT|n zW}jG!VZ_SybLEv?)D4-BaoX|ijCaszS+r~YxpLkXrbLd_Iz`{rxA6*0TfBew^CNRz x)84Utof`SFl&fm#{Ytm&j;ne%`2RaiXWf%v5wmrM$7hfeJYD@<);T3K0RVcQRh0k$ literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-close.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-close.png new file mode 100644 index 0000000000000000000000000000000000000000..9524e424e042b59d70d3cdf64024c9165e0d780d GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-V5X;wV~EA+qmv!^7!)~}Tl-I5zB&En$&}_R zZ`+z# zk7(Ew8ECWi%>ls;Jn`!%-*ZgsYq)xw;Y5!~(O+}F`|sRBzU3dZ0b0u7>FVdQ&MBb@ E0FuQ@od5s; literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-ctrl-alt-del.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-ctrl-alt-del.png new file mode 100644 index 0000000000000000000000000000000000000000..fad2d03ffc4c5f1c585ff982309fff7c84e1b15e GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs)$hP#xz@_|CKnIRD+&iT2y zsd*(pE(3#eQEFmIYKlU6W=V#EyQgnJie4%^P<)!Fi(`nz>8q0ixsDicxRjneShVG1 zTCwr>#i46_Pdn`^bKsDajyDONv}tjFPS)>+kKUK&T>dTR+1om&_JiEUcvU4ng};KL zeD5c$;%ZovlV-l^1G{H`|Eiml=i6{D>HFEnr*OSWEzWO^NxqB9S2H(bVTMQVB(>kn zIkf6`tf=iS`GtQqzjuDgzIv*kf93at*10?X|2M8>Ja=~W1i8z%RY6YoboFyt=akR{ E0Jj=-Y5)KL literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-ctrl.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-ctrl.png new file mode 100644 index 0000000000000000000000000000000000000000..baf54f14edbca4a99bbc114acce052d726c55c81 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs)y9P6J5m;r@kGeaUuobz*Y zQ}arITm}Z`qSVBa)D(sC%#sWRcTeAd6une-pm>U>i(`nz>AS&>d(v*XoH22SNf<)D`iDY70ePWi75n0H>Z zyVX}=dBn%AyjRQK$!8;-MT+OLG}_)Usv|qtjs({Caag(7XgK2GeaUuobz*Y zQ}arITm}Z`qSVBa)D(sC%#sWRcTeAd6une-p!j4@7sn8b(@Q4>avfFRVKKkd{50*> z?5AnB``i83E}K~9D4^_Yq{YzTdd6piAfrQ3O2-K^l}gr00ipZY-Z=eJzOwE1zJ#}K zswE#j7c`x@d{cH(RpH-H$4>>A`Y}v8Tvc{iVRG{{CZqdz=FEFz>5wS0z+mmO_q!vn z{`&Rs?>4LCCyzP)@A|AHD#K(~`XEyN-3-ZnPnf*;dbd|49aaaq+tbz0Wt~$(69C3T BbrApn literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-info.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-info.png new file mode 100644 index 0000000000000000000000000000000000000000..5c3f0e03f397124e1ddfbff1bd18e43144f661fd GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs)e=G^mG(||&tBA4z=qPddb76(J=m^1rT=4?!pQK$!8;-MT+OLG}_)Usv|qtjs*d>SoOTZ-GLxnIRD+&iT2y zsd*(pE(3#eQEFmIYKlU6W=V#EyQgnJie4%^P`ty_#WBR<^wvp%e1{Dp=fS?83{1OT_tZ5sdp literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-options.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-options.png new file mode 100644 index 0000000000000000000000000000000000000000..29bcd33d049d817ffd96b7fc9333269339569b42 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs*JidWJfe*p@~W`;zRIOpf) zrskCZxeN@>MX8A;sVNHOnI#zt?w-B@DSD~wK=CF|7sn8b(^n^7AunyW z$YZ1jSSV43R`Q$ykM?_%lsLabJEify%oNZ zF>$BYA$`%6mQ5WTG6$T>QX?d8?^+!<#di*ST5(8w_LO!$rVU;N4Z)EN?$U2Ew5pDz4H8SUKBHx>Fp&h&Kkb6Mw<&;$VPB4;Q7 literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-rec.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-rec.png new file mode 100644 index 0000000000000000000000000000000000000000..13ddfc4755b44440bf5c2665bbe7d95b5080972c GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs*73jb_tPXL8vGeaUuobz*Y zQ}arITm}Z`qSVBa)D(sC%#sWRcTeAd6une-pm>s}i(`nz>9>;uxegd`IL}|q{`#U# zRNl0|C5wC3%D+vp=jPy2%(yb)2$z{yAVb#LIH40~eu~XvXPYzA?o;5GJxmF+J1kt^ zKJQ$&CsnwQK$!8;-MT+OLG}_)Usv|qtjs(L*3NH(41hwinIRD+&iT2y zsd*(pE(3#eQEFmIYKlU6W=V#EyQgnJie4%^P`t*|#WBR<^wMBQz5@yz%#RLN+?sc7 z{?5Zrk0eT^EXBHX7cfnjoFV$+L)^oHjAJ=d&lL5FhO7@wTphGsDuDal-M_icPbb+a zx6blmk-L*tBw2aHA&>(-UHx3vIVCg!0DHP*d;kCd literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-save.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-save.png new file mode 100644 index 0000000000000000000000000000000000000000..0ac175b34c83e1ebd41db4572810880cfec80528 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs(n3eVMiI)Os6nIRD+&iT2y zsd*(pE(3#eQEFmIYKlU6W=V#EyQgnJie4%^P(0Jq#WBR<^wG)QTn7|*T-1*?pS@^v zYj@;sjixP4xiJzaoIK`Dj%SG4sN}irypNHbWkar1EifMA$y(u9H@JAwSD}Cdf6Ou6{1- HoD!M<&zD%y literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-win.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-win.png new file mode 100644 index 0000000000000000000000000000000000000000..f0815c344eff49223c3bd05590f9f2bd8251a251 GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QK$!8;-MT+OLG}_)Usv|qtjs)03cvULi~|bEW`;zRIOpf) zrskCZxeN@>MX8A;sVNHOnI#zt?w-B@DSD~wK=JvWE{-7^VGyX`!;ScmbXJIe=M=_?SxfZd7RGgLVJdKu zWQf}QcAqj(3tOLH!)=cD@|oGQL70(Y)*K0-;4x1Z#}JFtYbRy*9d_Voew*>$^v;?$XKmDr z|MO^7zFifn>*^=MW|yGBsp@&oM`r5G4vt4o(R*E5!i8?!tNdR-Q$cCZs)U|K%jf5m z7isu;&X^fDr{czsr`=zE^j?k2Z)bQTDj_6N_?ll@MX__Y*t0nd4Ti_&#}zJ@lwp3i z=g~f^=BUZWmnY@?z7($L?NnbECUi}Ud+YijAtPb40-3|tJa&9=D0v+Zh literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-fit.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/button-zoom-fit.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c090516816f782690c49ddf4496d072f16712d GIT binary patch literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G^tAk28_ZrvZCAbW|YuPgfvRk%(!oAY&eSx;TbdoL)NV;>5!WJTBp$TnmNG zrtT1NNe@zg=ki@Jp+Kf)`p&-xAKYwL2|J^6Atf!~>b}-hO-mj)U1C(>eGfS#jY& zw-HO&ITy)Jzp($QK$!8;-MT+OLG}_)Usv|qtjs(Lf-}NqKLZNMW`;zRIOpf) zrskCZxeN@>MX8A;sVNHOnI#zt?w-B@DSD~wK=DRT7sn8b(@TRL`3@*>Fh4r{%Vhqw z`8!Xa=~y`_cW;Nt5`mJQL70(Y)*K0-;C4?J#}JFtbAt}@9#P;aUiGfkygBcg&$Qn$ zRSgbV=jBtj7Und2I`Mm6jM`n9&8xyI9ii_~B=OEwMc4IQ<&JAyQL70(Y)*K0-;C4?J#}JFtb0=NoJz~J&>|0vhdUjGyn%ZN# zcPl1wE!p6ibB)VW#^k*)`nND`spWU-Q!?Skl=40P^ zlvErqF)le9_INqqDT19AI8MnfcoZ i$7fFtaPK{*a9Mobyai&Jwr(IVFnGH9xvX<>&kwIn}sg2e6AOocg2MsXEC14`c(6CT;V`S~JjqeO4Hj-_=(X9gfZgfD Tmdu+#^B6o`{an^LB{Ts5WC1k{ literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-128x128.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..a5d5a029447b25324344b1cb48ebd042ad659501 GIT binary patch literal 13783 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 Vzx}ey>>swQq^O)o%@2dX{{yOfGI9U_ literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-16x16.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..2f89c7022389f3f675182ca04aacaa62da166075 GIT binary patch literal 694 zcmV;n0!jUeP)PAMfe%7TR^qOLn(?!EKQoqON+z5nxA%>5|i>#3*yozDOCoYV8TBuTCTBY>Q9 z&dhMm@ec@az?h5USOCril)g=|V%2JDb8~csA^(Fw^B|K689sZGJ6A6gRmx_kKhXuPX#g^@X8i{4KYLB_%V&m;?x*l^ zl58&5RAWdW!sc5LMUkQ?k|dqkyG@c*DVLO_u_oWY)uq8hx^n)MqNt{*Ce2o+G*^78 zXf{z4MQSTBK;{83IjZHaEL&7#d}5RXJ3cXVa2@#%{q(-+C#b$amLM5nYkxDNZ(t*D zCQh)Tx4`oJG#OXI`xy=$$#Ls;4t<807*qoM6N<$f~?~^T>t<8 literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-32x32.png b/plugins/vnc/src/main/resources/com/glavsoft/viewer/images/tightvnc-logo-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..9c5e1e7a80cac5ad9def51fd371f38f0ffc73c55 GIT binary patch literal 1388 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 L*HKP*v9|vQWV~^W literal 0 HcmV?d00001 diff --git a/plugins/vnc/src/main/resources/icons/actualZoom.svg b/plugins/vnc/src/main/resources/icons/actualZoom.svg new file mode 100644 index 0000000..37507c0 --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/actualZoom.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/vnc/src/main/resources/icons/actualZoom_dark.svg b/plugins/vnc/src/main/resources/icons/actualZoom_dark.svg new file mode 100644 index 0000000..e497a8f --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/actualZoom_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/vnc/src/main/resources/icons/ctrlAltDel.svg b/plugins/vnc/src/main/resources/icons/ctrlAltDel.svg new file mode 100644 index 0000000..f0680c2 --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/ctrlAltDel.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/plugins/vnc/src/main/resources/icons/ctrlAltDel_dark.svg b/plugins/vnc/src/main/resources/icons/ctrlAltDel_dark.svg new file mode 100644 index 0000000..4965426 --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/ctrlAltDel_dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/plugins/vnc/src/main/resources/icons/zoomIn.svg b/plugins/vnc/src/main/resources/icons/zoomIn.svg new file mode 100644 index 0000000..dc4380e --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/zoomIn.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/vnc/src/main/resources/icons/zoomIn_dark.svg b/plugins/vnc/src/main/resources/icons/zoomIn_dark.svg new file mode 100644 index 0000000..39cb0f6 --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/zoomIn_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/vnc/src/main/resources/icons/zoomOut.svg b/plugins/vnc/src/main/resources/icons/zoomOut.svg new file mode 100644 index 0000000..90d8d41 --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/zoomOut.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/vnc/src/main/resources/icons/zoomOut_dark.svg b/plugins/vnc/src/main/resources/icons/zoomOut_dark.svg new file mode 100644 index 0000000..41c92b4 --- /dev/null +++ b/plugins/vnc/src/main/resources/icons/zoomOut_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/MySurface.kt b/plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/MySurface.kt new file mode 100644 index 0000000..2eedee8 --- /dev/null +++ b/plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/MySurface.kt @@ -0,0 +1,9 @@ +package app.termora.plugins.vnc + +import com.glavsoft.rfb.protocol.Protocol +import com.glavsoft.viewer.settings.LocalMouseCursorShape +import com.glavsoft.viewer.swing.Surface + +class MySurface(protocol: Protocol) : Surface(protocol, 0.5, LocalMouseCursorShape.NO_CURSOR) { + +} \ No newline at end of file diff --git a/plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/RfbClientTest.kt b/plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/RfbClientTest.kt new file mode 100644 index 0000000..acf98ff --- /dev/null +++ b/plugins/vnc/src/test/kotlin/app/termora/plugins/vnc/RfbClientTest.kt @@ -0,0 +1,62 @@ +package app.termora.plugins.vnc + +import com.glavsoft.rfb.IRfbSessionListener +import com.glavsoft.rfb.encoding.EncodingType +import com.glavsoft.rfb.protocol.Protocol +import com.glavsoft.rfb.protocol.ProtocolSettings +import com.glavsoft.transport.BaudrateMeter +import com.glavsoft.transport.Transport +import com.glavsoft.viewer.swing.ClipboardControllerImpl +import java.awt.BorderLayout +import java.awt.Dimension +import java.net.InetSocketAddress +import java.net.Socket +import javax.swing.JFrame +import javax.swing.JPanel +import javax.swing.JTextField +import javax.swing.SwingUtilities + + +class RfbClientTest { + + // @Test + fun myTest() { + + SwingUtilities.invokeLater { + val socket = Socket() + socket.connect(InetSocketAddress("10.211.55.14", 5900)) + socket.tcpNoDelay = true + + val transport = Transport(socket) + transport.setBaudrateMeter(BaudrateMeter()) + val protocolSettings = ProtocolSettings.getDefaultSettings() + protocolSettings.preferredEncoding = EncodingType.ZRLE + val protocol = Protocol(transport, { "123456" }, protocolSettings) + protocol.handshake() + val surface = MySurface(protocol) + + try { + protocol.startNormalHandling(object : IRfbSessionListener { + override fun rfbSessionStopped(p0: String?) { + + } + + }, surface, ClipboardControllerImpl(protocol, "GBK")) + } catch (e: Exception) { + e.printStackTrace() + } + + + val frame = JFrame() + val panel = JPanel(BorderLayout()) + panel.add(JTextField(), BorderLayout.NORTH) + panel.add(surface, BorderLayout.CENTER) + frame.contentPane.add(panel) + frame.size = Dimension(1024, 800) + frame.setLocationRelativeTo(null) + frame.isVisible = true + } + Thread.currentThread().join() + } + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4c5c3d2..514b681 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,4 @@ include("plugins:geo") include("plugins:webdav") include("plugins:smb") include("plugins:serial") +include("plugins:vnc") diff --git a/src/main/kotlin/app/termora/DynamicIcon.kt b/src/main/kotlin/app/termora/DynamicIcon.kt index 6bd6344..7c47888 100644 --- a/src/main/kotlin/app/termora/DynamicIcon.kt +++ b/src/main/kotlin/app/termora/DynamicIcon.kt @@ -2,10 +2,22 @@ package app.termora import com.formdev.flatlaf.extras.FlatSVGIcon -open class DynamicIcon(name: String, private val darkName: String = name, val allowColorFilter: Boolean = true) : - FlatSVGIcon(name) { +open class DynamicIcon( + name: String, + private val darkName: String = name, + val allowColorFilter: Boolean = true, + loader: ClassLoader? +) : FlatSVGIcon(name, loader) { constructor(name: String) : this(name, name) - val dark by lazy { DynamicIcon(darkName, name, allowColorFilter) } + constructor( + name: String, + darkName: String = name, + allowColorFilter: Boolean = true, + ) : this(name, darkName, allowColorFilter, null) { + + } + + val dark by lazy { DynamicIcon(darkName, name, allowColorFilter, classLoader) } }