From 49b779dcaf03e3598d2709b321e20ea029b25163 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Wed, 1 Oct 2014 23:04:51 +0100 Subject: Convert to gradle build system --- app/app.iml | 85 + app/build.gradle | 34 + app/proguard.cfg | 46 + .../java/org/connectbot/HostBeanTest.java | 96 + .../java/org/connectbot/HostListActivityTest.java | 47 + .../java/org/connectbot/SelectionAreaTest.java | 141 + .../java/org/connectbot/SettingsActivityTest.java | 47 + .../java/org/connectbot/TerminalBridgeTest.java | 105 + .../java/org/connectbot/mock/BeanTestCase.java | 229 ++ .../java/org/connectbot/mock/NullOutputStream.java | 33 + .../java/org/connectbot/mock/NullTransport.java | 138 + .../java/org/connectbot/util/PubkeyUtilsTest.java | 345 +++ app/src/main/AndroidManifest.xml | 96 + app/src/main/assets/help/Hints.html | 13 + app/src/main/assets/help/PhysicalKeyboard.html | 62 + app/src/main/assets/help/ScreenGestures.html | 38 + app/src/main/assets/help/VirtualKeyboard.html | 49 + app/src/main/java/com/google/ase/Exec.java | 69 + app/src/main/java/com/jcraft/jzlib/Adler32.java | 94 + app/src/main/java/com/jcraft/jzlib/Deflate.java | 1623 +++++++++++ app/src/main/java/com/jcraft/jzlib/InfBlocks.java | 614 ++++ app/src/main/java/com/jcraft/jzlib/InfCodes.java | 605 ++++ app/src/main/java/com/jcraft/jzlib/InfTree.java | 520 ++++ app/src/main/java/com/jcraft/jzlib/Inflate.java | 374 +++ app/src/main/java/com/jcraft/jzlib/JZlib.java | 67 + app/src/main/java/com/jcraft/jzlib/StaticTree.java | 149 + app/src/main/java/com/jcraft/jzlib/Tree.java | 365 +++ .../main/java/com/jcraft/jzlib/ZInputStream.java | 149 + .../main/java/com/jcraft/jzlib/ZOutputStream.java | 156 + app/src/main/java/com/jcraft/jzlib/ZStream.java | 211 ++ .../java/com/jcraft/jzlib/ZStreamException.java | 44 + .../java/com/trilead/ssh2/AuthAgentCallback.java | 64 + .../java/com/trilead/ssh2/ChannelCondition.java | 61 + app/src/main/java/com/trilead/ssh2/Connection.java | 1628 +++++++++++ .../main/java/com/trilead/ssh2/ConnectionInfo.java | 53 + .../java/com/trilead/ssh2/ConnectionMonitor.java | 34 + .../java/com/trilead/ssh2/DHGexParameters.java | 121 + .../main/java/com/trilead/ssh2/DebugLogger.java | 23 + .../com/trilead/ssh2/DynamicPortForwarder.java | 67 + .../main/java/com/trilead/ssh2/HTTPProxyData.java | 83 + .../java/com/trilead/ssh2/HTTPProxyException.java | 29 + .../java/com/trilead/ssh2/InteractiveCallback.java | 55 + app/src/main/java/com/trilead/ssh2/KnownHosts.java | 844 ++++++ .../java/com/trilead/ssh2/LocalPortForwarder.java | 63 + .../com/trilead/ssh2/LocalStreamForwarder.java | 78 + app/src/main/java/com/trilead/ssh2/ProxyData.java | 15 + app/src/main/java/com/trilead/ssh2/SCPClient.java | 729 +++++ .../main/java/com/trilead/ssh2/SFTPException.java | 91 + .../main/java/com/trilead/ssh2/SFTPv3Client.java | 1388 +++++++++ .../com/trilead/ssh2/SFTPv3DirectoryEntry.java | 38 + .../com/trilead/ssh2/SFTPv3FileAttributes.java | 145 + .../java/com/trilead/ssh2/SFTPv3FileHandle.java | 45 + .../com/trilead/ssh2/ServerHostKeyVerifier.java | 31 + app/src/main/java/com/trilead/ssh2/Session.java | 530 ++++ .../main/java/com/trilead/ssh2/StreamGobbler.java | 229 ++ .../trilead/ssh2/auth/AuthenticationManager.java | 466 +++ .../ssh2/channel/AuthAgentForwardThread.java | 579 ++++ .../java/com/trilead/ssh2/channel/Channel.java | 207 ++ .../trilead/ssh2/channel/ChannelInputStream.java | 86 + .../com/trilead/ssh2/channel/ChannelManager.java | 1756 ++++++++++++ .../trilead/ssh2/channel/ChannelOutputStream.java | 71 + .../trilead/ssh2/channel/DynamicAcceptThread.java | 284 ++ .../trilead/ssh2/channel/IChannelWorkerThread.java | 13 + .../trilead/ssh2/channel/LocalAcceptThread.java | 135 + .../trilead/ssh2/channel/RemoteAcceptThread.java | 103 + .../trilead/ssh2/channel/RemoteForwardingData.java | 17 + .../ssh2/channel/RemoteX11AcceptThread.java | 240 ++ .../com/trilead/ssh2/channel/StreamForwarder.java | 113 + .../com/trilead/ssh2/channel/X11ServerData.java | 16 + .../ssh2/compression/CompressionFactory.java | 96 + .../com/trilead/ssh2/compression/ICompressor.java | 32 + .../java/com/trilead/ssh2/compression/Zlib.java | 130 + .../com/trilead/ssh2/compression/ZlibOpenSSH.java | 35 + .../main/java/com/trilead/ssh2/crypto/Base64.java | 148 + .../com/trilead/ssh2/crypto/CryptoWishList.java | 26 + .../java/com/trilead/ssh2/crypto/KeyMaterial.java | 91 + .../java/com/trilead/ssh2/crypto/PEMDecoder.java | 494 ++++ .../java/com/trilead/ssh2/crypto/PEMStructure.java | 17 + .../com/trilead/ssh2/crypto/SimpleDERReader.java | 229 ++ .../java/com/trilead/ssh2/crypto/cipher/AES.java | 698 +++++ .../trilead/ssh2/crypto/cipher/BlockCipher.java | 16 + .../ssh2/crypto/cipher/BlockCipherFactory.java | 115 + .../com/trilead/ssh2/crypto/cipher/BlowFish.java | 403 +++ .../com/trilead/ssh2/crypto/cipher/CBCMode.java | 78 + .../com/trilead/ssh2/crypto/cipher/CTRMode.java | 62 + .../ssh2/crypto/cipher/CipherInputStream.java | 144 + .../ssh2/crypto/cipher/CipherOutputStream.java | 142 + .../java/com/trilead/ssh2/crypto/cipher/DES.java | 373 +++ .../com/trilead/ssh2/crypto/cipher/DESede.java | 105 + .../com/trilead/ssh2/crypto/cipher/NullCipher.java | 35 + .../com/trilead/ssh2/crypto/dh/DhExchange.java | 132 + .../trilead/ssh2/crypto/dh/DhGroupExchange.java | 113 + .../com/trilead/ssh2/crypto/dh/EcDhExchange.java | 106 + .../trilead/ssh2/crypto/dh/GenericDhExchange.java | 93 + .../ssh2/crypto/digest/HashForSSH2Types.java | 91 + .../java/com/trilead/ssh2/crypto/digest/MAC.java | 157 + app/src/main/java/com/trilead/ssh2/log/Logger.java | 54 + .../ssh2/packets/PacketChannelAuthAgentReq.java | 33 + .../packets/PacketChannelOpenConfirmation.java | 66 + .../ssh2/packets/PacketChannelOpenFailure.java | 66 + .../ssh2/packets/PacketChannelTrileadPing.java | 35 + .../ssh2/packets/PacketChannelWindowAdjust.java | 57 + .../com/trilead/ssh2/packets/PacketDisconnect.java | 57 + .../packets/PacketGlobalCancelForwardRequest.java | 42 + .../ssh2/packets/PacketGlobalForwardRequest.java | 41 + .../ssh2/packets/PacketGlobalTrileadPing.java | 32 + .../com/trilead/ssh2/packets/PacketIgnore.java | 59 + .../com/trilead/ssh2/packets/PacketKexDHInit.java | 31 + .../com/trilead/ssh2/packets/PacketKexDHReply.java | 53 + .../trilead/ssh2/packets/PacketKexDhGexGroup.java | 50 + .../trilead/ssh2/packets/PacketKexDhGexInit.java | 33 + .../trilead/ssh2/packets/PacketKexDhGexReply.java | 56 + .../ssh2/packets/PacketKexDhGexRequest.java | 39 + .../ssh2/packets/PacketKexDhGexRequestOld.java | 34 + .../com/trilead/ssh2/packets/PacketKexInit.java | 165 ++ .../com/trilead/ssh2/packets/PacketNewKeys.java | 46 + .../ssh2/packets/PacketOpenDirectTCPIPChannel.java | 56 + .../ssh2/packets/PacketOpenSessionChannel.java | 62 + .../trilead/ssh2/packets/PacketServiceAccept.java | 61 + .../trilead/ssh2/packets/PacketServiceRequest.java | 52 + .../ssh2/packets/PacketSessionExecCommand.java | 39 + .../ssh2/packets/PacketSessionPtyRequest.java | 57 + .../ssh2/packets/PacketSessionPtyResize.java | 40 + .../ssh2/packets/PacketSessionStartShell.java | 36 + .../packets/PacketSessionSubsystemRequest.java | 40 + .../ssh2/packets/PacketSessionX11Request.java | 53 + .../trilead/ssh2/packets/PacketUserauthBanner.java | 60 + .../ssh2/packets/PacketUserauthFailure.java | 53 + .../ssh2/packets/PacketUserauthInfoRequest.java | 84 + .../ssh2/packets/PacketUserauthInfoResponse.java | 35 + .../packets/PacketUserauthRequestInteractive.java | 42 + .../ssh2/packets/PacketUserauthRequestNone.java | 61 + .../packets/PacketUserauthRequestPassword.java | 67 + .../packets/PacketUserauthRequestPublicKey.java | 65 + .../java/com/trilead/ssh2/packets/Packets.java | 149 + .../java/com/trilead/ssh2/packets/TypesReader.java | 177 ++ .../java/com/trilead/ssh2/packets/TypesWriter.java | 169 ++ .../java/com/trilead/ssh2/sftp/AttrTextHints.java | 38 + .../java/com/trilead/ssh2/sftp/AttribBits.java | 129 + .../java/com/trilead/ssh2/sftp/AttribFlags.java | 112 + .../com/trilead/ssh2/sftp/AttribPermissions.java | 32 + .../java/com/trilead/ssh2/sftp/AttribTypes.java | 28 + .../java/com/trilead/ssh2/sftp/ErrorCodes.java | 104 + .../main/java/com/trilead/ssh2/sftp/OpenFlags.java | 223 ++ .../main/java/com/trilead/ssh2/sftp/Packet.java | 43 + .../com/trilead/ssh2/signature/DSASHA1Verify.java | 255 ++ .../trilead/ssh2/signature/ECDSASHA2Verify.java | 487 ++++ .../com/trilead/ssh2/signature/RSASHA1Verify.java | 180 ++ .../trilead/ssh2/transport/ClientServerHello.java | 125 + .../com/trilead/ssh2/transport/KexManager.java | 671 +++++ .../com/trilead/ssh2/transport/KexParameters.java | 24 + .../java/com/trilead/ssh2/transport/KexState.java | 33 + .../com/trilead/ssh2/transport/MessageHandler.java | 14 + .../trilead/ssh2/transport/NegotiateException.java | 12 + .../ssh2/transport/NegotiatedParameters.java | 22 + .../ssh2/transport/TransportConnection.java | 343 +++ .../trilead/ssh2/transport/TransportManager.java | 802 ++++++ .../java/com/trilead/ssh2/util/TimeoutService.java | 149 + .../main/java/com/trilead/ssh2/util/Tokenizer.java | 51 + .../java/de/mud/telnet/TelnetProtocolHandler.java | 678 +++++ app/src/main/java/de/mud/terminal/Precomposer.java | 1052 +++++++ app/src/main/java/de/mud/terminal/VDUBuffer.java | 854 ++++++ app/src/main/java/de/mud/terminal/VDUDisplay.java | 40 + app/src/main/java/de/mud/terminal/VDUInput.java | 90 + app/src/main/java/de/mud/terminal/vt320.java | 3032 ++++++++++++++++++++ .../net/sourceforge/jsocks/Authentication.java | 34 + .../net/sourceforge/jsocks/AuthenticationNone.java | 17 + .../main/java/net/sourceforge/jsocks/Proxy.java | 404 +++ .../java/net/sourceforge/jsocks/ProxyMessage.java | 109 + .../java/net/sourceforge/jsocks/ProxyServer.java | 591 ++++ .../java/net/sourceforge/jsocks/Socks4Message.java | 171 ++ .../java/net/sourceforge/jsocks/Socks4Proxy.java | 107 + .../sourceforge/jsocks/Socks5DatagramSocket.java | 460 +++ .../java/net/sourceforge/jsocks/Socks5Message.java | 292 ++ .../java/net/sourceforge/jsocks/Socks5Proxy.java | 231 ++ .../net/sourceforge/jsocks/SocksException.java | 80 + .../net/sourceforge/jsocks/SocksServerSocket.java | 164 ++ .../java/net/sourceforge/jsocks/SocksSocket.java | 291 ++ .../net/sourceforge/jsocks/UDPEncapsulation.java | 29 + .../net/sourceforge/jsocks/UDPRelayServer.java | 212 ++ .../jsocks/server/ServerAuthenticator.java | 120 + .../jsocks/server/ServerAuthenticatorNone.java | 169 ++ .../harmony/niochar/charset/additional/IBM437.java | 423 +++ .../main/java/org/connectbot/ActionBarWrapper.java | 83 + .../main/java/org/connectbot/ColorsActivity.java | 331 +++ .../main/java/org/connectbot/ConsoleActivity.java | 1175 ++++++++ .../org/connectbot/GeneratePubkeyActivity.java | 353 +++ app/src/main/java/org/connectbot/HelpActivity.java | 77 + .../java/org/connectbot/HelpTopicActivity.java | 49 + .../java/org/connectbot/HostEditorActivity.java | 433 +++ .../main/java/org/connectbot/HostListActivity.java | 570 ++++ .../org/connectbot/PortForwardListActivity.java | 425 +++ .../java/org/connectbot/PubkeyListActivity.java | 673 +++++ .../main/java/org/connectbot/SettingsActivity.java | 60 + .../main/java/org/connectbot/StrictModeSetup.java | 23 + app/src/main/java/org/connectbot/TerminalView.java | 452 +++ .../main/java/org/connectbot/WizardActivity.java | 105 + .../java/org/connectbot/bean/AbstractBean.java | 49 + .../main/java/org/connectbot/bean/HostBean.java | 317 ++ .../java/org/connectbot/bean/PortForwardBean.java | 239 ++ .../main/java/org/connectbot/bean/PubkeyBean.java | 234 ++ .../java/org/connectbot/bean/SelectionArea.java | 201 ++ .../java/org/connectbot/service/BackupAgent.java | 73 + .../java/org/connectbot/service/BackupWrapper.java | 71 + .../service/BridgeDisconnectedListener.java | 22 + .../org/connectbot/service/ConnectionNotifier.java | 192 ++ .../connectbot/service/ConnectivityReceiver.java | 154 + .../service/FontSizeChangedListener.java | 31 + .../java/org/connectbot/service/KeyEventUtil.java | 98 + .../java/org/connectbot/service/PromptHelper.java | 159 + .../main/java/org/connectbot/service/Relay.java | 145 + .../org/connectbot/service/TerminalBridge.java | 1018 +++++++ .../connectbot/service/TerminalKeyListener.java | 558 ++++ .../org/connectbot/service/TerminalManager.java | 714 +++++ .../org/connectbot/transport/AbsTransport.java | 254 ++ .../main/java/org/connectbot/transport/Local.java | 219 ++ .../main/java/org/connectbot/transport/SSH.java | 958 +++++++ .../main/java/org/connectbot/transport/Telnet.java | 330 +++ .../org/connectbot/transport/TransportFactory.java | 132 + app/src/main/java/org/connectbot/util/Colors.java | 91 + .../java/org/connectbot/util/EastAsianWidth.java | 75 + .../main/java/org/connectbot/util/Encryptor.java | 205 ++ .../java/org/connectbot/util/EntropyDialog.java | 50 + .../main/java/org/connectbot/util/EntropyView.java | 169 ++ .../java/org/connectbot/util/HelpTopicView.java | 62 + .../java/org/connectbot/util/HostDatabase.java | 766 +++++ .../org/connectbot/util/OnDbWrittenListener.java | 26 + .../connectbot/util/OnEntropyGatheredListener.java | 22 + .../org/connectbot/util/PreferenceConstants.java | 90 + .../java/org/connectbot/util/PubkeyDatabase.java | 329 +++ .../main/java/org/connectbot/util/PubkeyUtils.java | 352 +++ .../connectbot/util/RobustSQLiteOpenHelper.java | 133 + .../org/connectbot/util/UberColorPickerDialog.java | 982 +++++++ .../java/org/connectbot/util/VolumePreference.java | 72 + .../main/java/org/connectbot/util/XmlBuilder.java | 71 + app/src/main/java/org/keyczar/jce/EcCore.java | 679 +++++ .../openintents/intents/FileManagerIntents.java | 66 + app/src/main/jni/com_google_ase_Exec.cpp | 194 ++ app/src/main/jni/com_google_ase_Exec.h | 45 + app/src/main/res/anim/fade_out_delayed.xml | 27 + app/src/main/res/anim/fade_stay_hidden.xml | 26 + app/src/main/res/anim/keyboard_fade_in.xml | 25 + app/src/main/res/anim/keyboard_fade_out.xml | 25 + app/src/main/res/anim/slide_left_in.xml | 24 + app/src/main/res/anim/slide_left_out.xml | 24 + app/src/main/res/anim/slide_right_in.xml | 24 + app/src/main/res/anim/slide_right_out.xml | 24 + app/src/main/res/color/blue.xml | 26 + app/src/main/res/color/green.xml | 26 + app/src/main/res/color/red.xml | 26 + app/src/main/res/drawable-hdpi/icon.png | Bin 0 -> 6394 bytes .../main/res/drawable-hdpi/notification_icon.png | Bin 0 -> 719 bytes app/src/main/res/drawable-mdpi-v6/icon.png | Bin 0 -> 3525 bytes app/src/main/res/drawable-mdpi/icon.png | Bin 0 -> 5657 bytes .../main/res/drawable-mdpi/notification_icon.png | Bin 0 -> 397 bytes app/src/main/res/drawable/button_ctrl.png | Bin 0 -> 3254 bytes app/src/main/res/drawable/button_esc.png | Bin 0 -> 3638 bytes app/src/main/res/drawable/button_keyboard.png | Bin 0 -> 3326 bytes app/src/main/res/drawable/connected.xml | 34 + .../res/drawable/highlight_disabled_pressed.9.png | Bin 0 -> 898 bytes app/src/main/res/drawable/ic_btn_back.png | Bin 0 -> 861 bytes app/src/main/res/drawable/ic_btn_next.png | Bin 0 -> 729 bytes app/src/main/res/drawable/icon_older.png | Bin 0 -> 5088 bytes app/src/main/res/drawable/pubkey.xml | 30 + app/src/main/res/drawable/pubkey_locked.png | Bin 0 -> 1487 bytes app/src/main/res/drawable/pubkey_unlocked.png | Bin 0 -> 1743 bytes app/src/main/res/layout-land/item_host.xml | 64 + app/src/main/res/layout-port/item_host.xml | 57 + app/src/main/res/layout/act_colors.xml | 71 + app/src/main/res/layout/act_console.xml | 164 ++ app/src/main/res/layout/act_generatepubkey.xml | 183 ++ app/src/main/res/layout/act_help.xml | 56 + app/src/main/res/layout/act_help_topic.xml | 33 + app/src/main/res/layout/act_hostlist.xml | 64 + app/src/main/res/layout/act_portforwardlist.xml | 40 + app/src/main/res/layout/act_pubkeylist.xml | 40 + app/src/main/res/layout/act_wizard.xml | 62 + app/src/main/res/layout/dia_changepassword.xml | 99 + app/src/main/res/layout/dia_gatherentropy.xml | 44 + app/src/main/res/layout/dia_password.xml | 38 + app/src/main/res/layout/dia_portforward.xml | 105 + app/src/main/res/layout/dia_resize.xml | 55 + app/src/main/res/layout/item_portforward.xml | 48 + app/src/main/res/layout/item_pubkey.xml | 54 + app/src/main/res/layout/item_terminal.xml | 37 + app/src/main/res/layout/wiz_eula.xml | 96 + app/src/main/res/raw/bell.ogg | Bin 0 -> 5090 bytes app/src/main/res/values-af/strings.xml | 42 + app/src/main/res/values-ar/strings.xml | 45 + app/src/main/res/values-be/strings.xml | 7 + app/src/main/res/values-bg/strings.xml | 62 + app/src/main/res/values-ca/strings.xml | 214 ++ app/src/main/res/values-cs/strings.xml | 202 ++ app/src/main/res/values-da/strings.xml | 214 ++ app/src/main/res/values-de/strings.xml | 221 ++ app/src/main/res/values-el/strings.xml | 6 + app/src/main/res/values-en-rCA/strings.xml | 10 + app/src/main/res/values-en-rGB/strings.xml | 29 + app/src/main/res/values-es/strings.xml | 216 ++ app/src/main/res/values-eu/strings.xml | 219 ++ app/src/main/res/values-fa/strings.xml | 2 + app/src/main/res/values-fi/strings.xml | 216 ++ app/src/main/res/values-fr/strings.xml | 221 ++ app/src/main/res/values-gl/strings.xml | 123 + app/src/main/res/values-he/strings.xml | 221 ++ app/src/main/res/values-hr/strings.xml | 94 + app/src/main/res/values-hu/strings.xml | 219 ++ app/src/main/res/values-id/strings.xml | 205 ++ app/src/main/res/values-is/strings.xml | 181 ++ app/src/main/res/values-it/strings.xml | 218 ++ app/src/main/res/values-ja/strings.xml | 205 ++ app/src/main/res/values-ka/strings.xml | 15 + app/src/main/res/values-ko/strings.xml | 192 ++ app/src/main/res/values-lt/strings.xml | 10 + app/src/main/res/values-lv/strings.xml | 2 + app/src/main/res/values-mk/strings.xml | 68 + app/src/main/res/values-nb/strings.xml | 214 ++ app/src/main/res/values-nl/strings.xml | 214 ++ app/src/main/res/values-oc/strings.xml | 91 + app/src/main/res/values-pl/strings.xml | 221 ++ app/src/main/res/values-pt-rBR/strings.xml | 220 ++ app/src/main/res/values-pt/strings.xml | 214 ++ app/src/main/res/values-ro/strings.xml | 55 + app/src/main/res/values-ru/strings.xml | 216 ++ app/src/main/res/values-sk/strings.xml | 214 ++ app/src/main/res/values-sl/strings.xml | 193 ++ app/src/main/res/values-sv/strings.xml | 214 ++ app/src/main/res/values-tr/strings.xml | 218 ++ app/src/main/res/values-uk/strings.xml | 27 + app/src/main/res/values-v11/styles.xml | 30 + app/src/main/res/values-v14/styles.xml | 30 + app/src/main/res/values-vi/strings.xml | 11 + app/src/main/res/values-zh-rCN/strings.xml | 221 ++ app/src/main/res/values-zh-rHK/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 221 ++ app/src/main/res/values/arrays.xml | 142 + app/src/main/res/values/notrans.xml | 25 + app/src/main/res/values/strings.xml | 500 ++++ app/src/main/res/values/styles.xml | 25 + app/src/main/res/values/version.xml | 4 + app/src/main/res/xml/host_prefs.xml | 125 + app/src/main/res/xml/preferences.xml | 177 ++ 342 files changed, 62079 insertions(+) create mode 100644 app/app.iml create mode 100644 app/build.gradle create mode 100644 app/proguard.cfg create mode 100644 app/src/androidTest/java/org/connectbot/HostBeanTest.java create mode 100644 app/src/androidTest/java/org/connectbot/HostListActivityTest.java create mode 100644 app/src/androidTest/java/org/connectbot/SelectionAreaTest.java create mode 100644 app/src/androidTest/java/org/connectbot/SettingsActivityTest.java create mode 100644 app/src/androidTest/java/org/connectbot/TerminalBridgeTest.java create mode 100644 app/src/androidTest/java/org/connectbot/mock/BeanTestCase.java create mode 100644 app/src/androidTest/java/org/connectbot/mock/NullOutputStream.java create mode 100644 app/src/androidTest/java/org/connectbot/mock/NullTransport.java create mode 100644 app/src/androidTest/java/org/connectbot/util/PubkeyUtilsTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/help/Hints.html create mode 100644 app/src/main/assets/help/PhysicalKeyboard.html create mode 100644 app/src/main/assets/help/ScreenGestures.html create mode 100644 app/src/main/assets/help/VirtualKeyboard.html create mode 100644 app/src/main/java/com/google/ase/Exec.java create mode 100644 app/src/main/java/com/jcraft/jzlib/Adler32.java create mode 100644 app/src/main/java/com/jcraft/jzlib/Deflate.java create mode 100644 app/src/main/java/com/jcraft/jzlib/InfBlocks.java create mode 100644 app/src/main/java/com/jcraft/jzlib/InfCodes.java create mode 100644 app/src/main/java/com/jcraft/jzlib/InfTree.java create mode 100644 app/src/main/java/com/jcraft/jzlib/Inflate.java create mode 100644 app/src/main/java/com/jcraft/jzlib/JZlib.java create mode 100644 app/src/main/java/com/jcraft/jzlib/StaticTree.java create mode 100644 app/src/main/java/com/jcraft/jzlib/Tree.java create mode 100644 app/src/main/java/com/jcraft/jzlib/ZInputStream.java create mode 100644 app/src/main/java/com/jcraft/jzlib/ZOutputStream.java create mode 100644 app/src/main/java/com/jcraft/jzlib/ZStream.java create mode 100644 app/src/main/java/com/jcraft/jzlib/ZStreamException.java create mode 100644 app/src/main/java/com/trilead/ssh2/AuthAgentCallback.java create mode 100644 app/src/main/java/com/trilead/ssh2/ChannelCondition.java create mode 100644 app/src/main/java/com/trilead/ssh2/Connection.java create mode 100644 app/src/main/java/com/trilead/ssh2/ConnectionInfo.java create mode 100644 app/src/main/java/com/trilead/ssh2/ConnectionMonitor.java create mode 100644 app/src/main/java/com/trilead/ssh2/DHGexParameters.java create mode 100644 app/src/main/java/com/trilead/ssh2/DebugLogger.java create mode 100644 app/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java create mode 100644 app/src/main/java/com/trilead/ssh2/HTTPProxyData.java create mode 100644 app/src/main/java/com/trilead/ssh2/HTTPProxyException.java create mode 100644 app/src/main/java/com/trilead/ssh2/InteractiveCallback.java create mode 100644 app/src/main/java/com/trilead/ssh2/KnownHosts.java create mode 100644 app/src/main/java/com/trilead/ssh2/LocalPortForwarder.java create mode 100644 app/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java create mode 100644 app/src/main/java/com/trilead/ssh2/ProxyData.java create mode 100644 app/src/main/java/com/trilead/ssh2/SCPClient.java create mode 100644 app/src/main/java/com/trilead/ssh2/SFTPException.java create mode 100644 app/src/main/java/com/trilead/ssh2/SFTPv3Client.java create mode 100644 app/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java create mode 100644 app/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java create mode 100644 app/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java create mode 100644 app/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java create mode 100644 app/src/main/java/com/trilead/ssh2/Session.java create mode 100644 app/src/main/java/com/trilead/ssh2/StreamGobbler.java create mode 100644 app/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/Channel.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/ChannelManager.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java create mode 100644 app/src/main/java/com/trilead/ssh2/channel/X11ServerData.java create mode 100644 app/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java create mode 100644 app/src/main/java/com/trilead/ssh2/compression/ICompressor.java create mode 100644 app/src/main/java/com/trilead/ssh2/compression/Zlib.java create mode 100644 app/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/Base64.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java create mode 100644 app/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java create mode 100644 app/src/main/java/com/trilead/ssh2/log/Logger.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/Packets.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/TypesReader.java create mode 100644 app/src/main/java/com/trilead/ssh2/packets/TypesWriter.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/AttribBits.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java create mode 100644 app/src/main/java/com/trilead/ssh2/sftp/Packet.java create mode 100644 app/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java create mode 100644 app/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java create mode 100644 app/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/KexManager.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/KexParameters.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/KexState.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/MessageHandler.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/NegotiateException.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/TransportConnection.java create mode 100644 app/src/main/java/com/trilead/ssh2/transport/TransportManager.java create mode 100644 app/src/main/java/com/trilead/ssh2/util/TimeoutService.java create mode 100644 app/src/main/java/com/trilead/ssh2/util/Tokenizer.java create mode 100644 app/src/main/java/de/mud/telnet/TelnetProtocolHandler.java create mode 100644 app/src/main/java/de/mud/terminal/Precomposer.java create mode 100644 app/src/main/java/de/mud/terminal/VDUBuffer.java create mode 100644 app/src/main/java/de/mud/terminal/VDUDisplay.java create mode 100644 app/src/main/java/de/mud/terminal/VDUInput.java create mode 100644 app/src/main/java/de/mud/terminal/vt320.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Authentication.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/AuthenticationNone.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Proxy.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/ProxyMessage.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/ProxyServer.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Socks4Message.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Socks4Proxy.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Socks5DatagramSocket.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Socks5Message.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/Socks5Proxy.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/SocksException.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/SocksServerSocket.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/SocksSocket.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/UDPEncapsulation.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/UDPRelayServer.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticator.java create mode 100644 app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticatorNone.java create mode 100644 app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java create mode 100644 app/src/main/java/org/connectbot/ActionBarWrapper.java create mode 100644 app/src/main/java/org/connectbot/ColorsActivity.java create mode 100644 app/src/main/java/org/connectbot/ConsoleActivity.java create mode 100644 app/src/main/java/org/connectbot/GeneratePubkeyActivity.java create mode 100644 app/src/main/java/org/connectbot/HelpActivity.java create mode 100644 app/src/main/java/org/connectbot/HelpTopicActivity.java create mode 100644 app/src/main/java/org/connectbot/HostEditorActivity.java create mode 100644 app/src/main/java/org/connectbot/HostListActivity.java create mode 100644 app/src/main/java/org/connectbot/PortForwardListActivity.java create mode 100644 app/src/main/java/org/connectbot/PubkeyListActivity.java create mode 100644 app/src/main/java/org/connectbot/SettingsActivity.java create mode 100644 app/src/main/java/org/connectbot/StrictModeSetup.java create mode 100644 app/src/main/java/org/connectbot/TerminalView.java create mode 100644 app/src/main/java/org/connectbot/WizardActivity.java create mode 100644 app/src/main/java/org/connectbot/bean/AbstractBean.java create mode 100644 app/src/main/java/org/connectbot/bean/HostBean.java create mode 100644 app/src/main/java/org/connectbot/bean/PortForwardBean.java create mode 100644 app/src/main/java/org/connectbot/bean/PubkeyBean.java create mode 100644 app/src/main/java/org/connectbot/bean/SelectionArea.java create mode 100644 app/src/main/java/org/connectbot/service/BackupAgent.java create mode 100644 app/src/main/java/org/connectbot/service/BackupWrapper.java create mode 100644 app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java create mode 100644 app/src/main/java/org/connectbot/service/ConnectionNotifier.java create mode 100644 app/src/main/java/org/connectbot/service/ConnectivityReceiver.java create mode 100644 app/src/main/java/org/connectbot/service/FontSizeChangedListener.java create mode 100644 app/src/main/java/org/connectbot/service/KeyEventUtil.java create mode 100644 app/src/main/java/org/connectbot/service/PromptHelper.java create mode 100644 app/src/main/java/org/connectbot/service/Relay.java create mode 100644 app/src/main/java/org/connectbot/service/TerminalBridge.java create mode 100644 app/src/main/java/org/connectbot/service/TerminalKeyListener.java create mode 100644 app/src/main/java/org/connectbot/service/TerminalManager.java create mode 100644 app/src/main/java/org/connectbot/transport/AbsTransport.java create mode 100644 app/src/main/java/org/connectbot/transport/Local.java create mode 100644 app/src/main/java/org/connectbot/transport/SSH.java create mode 100644 app/src/main/java/org/connectbot/transport/Telnet.java create mode 100644 app/src/main/java/org/connectbot/transport/TransportFactory.java create mode 100644 app/src/main/java/org/connectbot/util/Colors.java create mode 100644 app/src/main/java/org/connectbot/util/EastAsianWidth.java create mode 100644 app/src/main/java/org/connectbot/util/Encryptor.java create mode 100644 app/src/main/java/org/connectbot/util/EntropyDialog.java create mode 100644 app/src/main/java/org/connectbot/util/EntropyView.java create mode 100644 app/src/main/java/org/connectbot/util/HelpTopicView.java create mode 100644 app/src/main/java/org/connectbot/util/HostDatabase.java create mode 100644 app/src/main/java/org/connectbot/util/OnDbWrittenListener.java create mode 100644 app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java create mode 100644 app/src/main/java/org/connectbot/util/PreferenceConstants.java create mode 100644 app/src/main/java/org/connectbot/util/PubkeyDatabase.java create mode 100644 app/src/main/java/org/connectbot/util/PubkeyUtils.java create mode 100644 app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java create mode 100644 app/src/main/java/org/connectbot/util/UberColorPickerDialog.java create mode 100644 app/src/main/java/org/connectbot/util/VolumePreference.java create mode 100644 app/src/main/java/org/connectbot/util/XmlBuilder.java create mode 100644 app/src/main/java/org/keyczar/jce/EcCore.java create mode 100644 app/src/main/java/org/openintents/intents/FileManagerIntents.java create mode 100644 app/src/main/jni/com_google_ase_Exec.cpp create mode 100644 app/src/main/jni/com_google_ase_Exec.h create mode 100644 app/src/main/res/anim/fade_out_delayed.xml create mode 100644 app/src/main/res/anim/fade_stay_hidden.xml create mode 100644 app/src/main/res/anim/keyboard_fade_in.xml create mode 100644 app/src/main/res/anim/keyboard_fade_out.xml create mode 100644 app/src/main/res/anim/slide_left_in.xml create mode 100644 app/src/main/res/anim/slide_left_out.xml create mode 100644 app/src/main/res/anim/slide_right_in.xml create mode 100644 app/src/main/res/anim/slide_right_out.xml create mode 100644 app/src/main/res/color/blue.xml create mode 100644 app/src/main/res/color/green.xml create mode 100644 app/src/main/res/color/red.xml create mode 100644 app/src/main/res/drawable-hdpi/icon.png create mode 100644 app/src/main/res/drawable-hdpi/notification_icon.png create mode 100644 app/src/main/res/drawable-mdpi-v6/icon.png create mode 100644 app/src/main/res/drawable-mdpi/icon.png create mode 100644 app/src/main/res/drawable-mdpi/notification_icon.png create mode 100644 app/src/main/res/drawable/button_ctrl.png create mode 100644 app/src/main/res/drawable/button_esc.png create mode 100644 app/src/main/res/drawable/button_keyboard.png create mode 100644 app/src/main/res/drawable/connected.xml create mode 100644 app/src/main/res/drawable/highlight_disabled_pressed.9.png create mode 100644 app/src/main/res/drawable/ic_btn_back.png create mode 100644 app/src/main/res/drawable/ic_btn_next.png create mode 100644 app/src/main/res/drawable/icon_older.png create mode 100644 app/src/main/res/drawable/pubkey.xml create mode 100644 app/src/main/res/drawable/pubkey_locked.png create mode 100644 app/src/main/res/drawable/pubkey_unlocked.png create mode 100644 app/src/main/res/layout-land/item_host.xml create mode 100644 app/src/main/res/layout-port/item_host.xml create mode 100644 app/src/main/res/layout/act_colors.xml create mode 100644 app/src/main/res/layout/act_console.xml create mode 100644 app/src/main/res/layout/act_generatepubkey.xml create mode 100644 app/src/main/res/layout/act_help.xml create mode 100644 app/src/main/res/layout/act_help_topic.xml create mode 100644 app/src/main/res/layout/act_hostlist.xml create mode 100644 app/src/main/res/layout/act_portforwardlist.xml create mode 100644 app/src/main/res/layout/act_pubkeylist.xml create mode 100644 app/src/main/res/layout/act_wizard.xml create mode 100644 app/src/main/res/layout/dia_changepassword.xml create mode 100644 app/src/main/res/layout/dia_gatherentropy.xml create mode 100644 app/src/main/res/layout/dia_password.xml create mode 100644 app/src/main/res/layout/dia_portforward.xml create mode 100644 app/src/main/res/layout/dia_resize.xml create mode 100644 app/src/main/res/layout/item_portforward.xml create mode 100644 app/src/main/res/layout/item_pubkey.xml create mode 100644 app/src/main/res/layout/item_terminal.xml create mode 100644 app/src/main/res/layout/wiz_eula.xml create mode 100644 app/src/main/res/raw/bell.ogg create mode 100644 app/src/main/res/values-af/strings.xml create mode 100644 app/src/main/res/values-ar/strings.xml create mode 100644 app/src/main/res/values-be/strings.xml create mode 100644 app/src/main/res/values-bg/strings.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-da/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-en-rCA/strings.xml create mode 100644 app/src/main/res/values-en-rGB/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-eu/strings.xml create mode 100644 app/src/main/res/values-fa/strings.xml create mode 100644 app/src/main/res/values-fi/strings.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-gl/strings.xml create mode 100644 app/src/main/res/values-he/strings.xml create mode 100644 app/src/main/res/values-hr/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-id/strings.xml create mode 100644 app/src/main/res/values-is/strings.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-ka/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-lt/strings.xml create mode 100644 app/src/main/res/values-lv/strings.xml create mode 100644 app/src/main/res/values-mk/strings.xml create mode 100644 app/src/main/res/values-nb/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-oc/strings.xml create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-pt/strings.xml create mode 100644 app/src/main/res/values-ro/strings.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-sl/strings.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-v11/styles.xml create mode 100644 app/src/main/res/values-v14/styles.xml create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values-zh-rHK/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/notrans.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/version.xml create mode 100644 app/src/main/res/xml/host_prefs.xml create mode 100644 app/src/main/res/xml/preferences.xml (limited to 'app') diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..209bcb0 --- /dev/null +++ b/app/app.iml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..5e8e253 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 20 + buildToolsVersion "20.0.0" + + defaultConfig { + applicationId "org.connectbot" + minSdkVersion 4 + targetSdkVersion 15 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_5 + targetCompatibility JavaVersion.VERSION_1_5 + } + + ndk { + ldLibs "log" + } + + lintOptions { + abortOnError false + } + + testApplicationId "org.connectbot.tests" + testInstrumentationRunner "android.test.InstrumentationTestRunner" + } + + buildTypes { + release { + runProguard true + proguardFiles 'proguard.cfg' + } + } +} diff --git a/app/proguard.cfg b/app/proguard.cfg new file mode 100644 index 0000000..28ff286 --- /dev/null +++ b/app/proguard.cfg @@ -0,0 +1,46 @@ +#-keepattributes SourceFile,LineNumberTable +#-optimizationpasses 5 +-dontobfuscate +-dontoptimize +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-keep class org.connectbot.** +-keep public class com.trilead.ssh2.compression.* +-keep public class com.trilead.ssh2.crypto.* diff --git a/app/src/androidTest/java/org/connectbot/HostBeanTest.java b/app/src/androidTest/java/org/connectbot/HostBeanTest.java new file mode 100644 index 0000000..a252aca --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/HostBeanTest.java @@ -0,0 +1,96 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import org.connectbot.bean.HostBean; +import org.connectbot.mock.BeanTestCase; + +import android.test.AndroidTestCase; + +/** + * @author Kenny Root + * + */ +public class HostBeanTest extends AndroidTestCase { + private static final String[] FIELDS = { "nickname", "username", + "hostname", "port" }; + + HostBean host1; + HostBean host2; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + host1 = new HostBean(); + host1.setNickname("Home"); + host1.setUsername("bob"); + host1.setHostname("server.example.com"); + host1.setPort(22); + + host2 = new HostBean(); + host2.setNickname("Home"); + host2.setUsername("bob"); + host2.setHostname("server.example.com"); + host2.setPort(22); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testIdEquality() { + host1.setId(1); + host2.setId(1); + assertTrue(host1.equals(host2)); + assertTrue(host1.hashCode() == host2.hashCode()); + } + + public void testIdInequality() { + host1.setId(1); + host2.setId(2); + // HostBeans shouldn't be equal when their IDs are not the same + assertFalse("HostBeans are equal when their ID is different", host1 + .equals(host2)); + assertFalse("HostBean hash codes are equal when their ID is different", + host1.hashCode() == host2.hashCode()); + } + + public void testIdEquality2() { + host1.setId(1); + host2.setId(1); + host2.setNickname("Work"); + host2.setUsername("alice"); + host2.setHostname("client.example.com"); + assertTrue( + "HostBeans are not equal when their ID is the same but other fields are different!", + host1.equals(host2)); + assertTrue( + "HostBeans hashCodes are not equal when their ID is the same but other fields are different!", + host1.hashCode() == host2.hashCode()); + } + + public void testBeanMeetsEqualsContract() { + BeanTestCase.assertMeetsEqualsContract(HostBean.class, FIELDS); + } + + public void testBeanMeetsHashCodeContract() { + BeanTestCase.assertMeetsHashCodeContract(HostBean.class, FIELDS); + } +} diff --git a/app/src/androidTest/java/org/connectbot/HostListActivityTest.java b/app/src/androidTest/java/org/connectbot/HostListActivityTest.java new file mode 100644 index 0000000..3962c9a --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/HostListActivityTest.java @@ -0,0 +1,47 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import android.app.Activity; +import android.test.ActivityInstrumentationTestCase2; + +/** + * This is a simple framework for a test of an Application. See + * {@link android.test.ApplicationTestCase ApplicationTestCase} for more + * information on how to write and extend Application tests. + *

+ * To run this test, you can type: adb shell am instrument -w \ -e class + * org.connectbot.HostListActivityTest \ + * org.connectbot.tests/android.test.InstrumentationTestRunner + */ +public class HostListActivityTest extends ActivityInstrumentationTestCase2 { + private Activity mActivity; + + public HostListActivityTest() { + super("org.connectbot", HostListActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + setActivityInitialTouchMode(false); + + mActivity = getActivity(); + } +} diff --git a/app/src/androidTest/java/org/connectbot/SelectionAreaTest.java b/app/src/androidTest/java/org/connectbot/SelectionAreaTest.java new file mode 100644 index 0000000..93e0293 --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/SelectionAreaTest.java @@ -0,0 +1,141 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import org.connectbot.bean.SelectionArea; + +import android.test.AndroidTestCase; + +/** + * @author Kenny Root + * + */ +public class SelectionAreaTest extends AndroidTestCase { + private static final int WIDTH = 80; + private static final int HEIGHT = 24; + + public void testCreate() { + SelectionArea sa = new SelectionArea(); + + assertTrue(sa.getLeft() == 0); + assertTrue(sa.getRight() == 0); + assertTrue(sa.getTop() == 0); + assertTrue(sa.getBottom() == 0); + assertTrue(sa.isSelectingOrigin()); + } + + public void testCheckMovement() { + SelectionArea sa = new SelectionArea(); + + sa.setBounds(WIDTH, HEIGHT); + + sa.incrementColumn(); + + // Should be (1,0) to (1,0) + assertTrue(sa.getLeft() == 1); + assertTrue(sa.getTop() == 0); + assertTrue(sa.getRight() == 1); + assertTrue(sa.getBottom() == 0); + + sa.finishSelectingOrigin(); + assertFalse(sa.isSelectingOrigin()); + + sa.incrementColumn(); + sa.incrementColumn(); + + // Should be (1,0) to (3,0) + assertTrue(sa.getLeft() == 1); + assertTrue(sa.getTop() == 0); + assertTrue(sa.getRight() == 3); + assertTrue(sa.getBottom() == 0); + } + + public void testBounds() { + SelectionArea sa = new SelectionArea(); + + sa.setBounds(WIDTH, HEIGHT); + + for (int i = 0; i <= WIDTH; i++) + sa.decrementColumn(); + assertTrue("Left bound should be 0, but instead is " + sa.getLeft(), + sa.getLeft() == 0); + + for (int i = 0; i <= HEIGHT; i++) + sa.decrementRow(); + assertTrue("Top bound should be 0, but instead is " + sa.getLeft(), + sa.getTop() == 0); + + sa.finishSelectingOrigin(); + + for (int i = 0; i <= WIDTH * 2; i++) + sa.incrementColumn(); + + assertTrue("Left bound should be 0, but instead is " + sa.getLeft(), + sa.getLeft() == 0); + assertTrue("Right bound should be " + (WIDTH - 1) + ", but instead is " + sa.getRight(), + sa.getRight() == (WIDTH - 1)); + + for (int i = 0; i <= HEIGHT * 2; i++) + sa.incrementRow(); + + assertTrue("Bottom bound should be " + (HEIGHT - 1) + ", but instead is " + sa.getBottom(), + sa.getBottom() == (HEIGHT - 1)); + assertTrue("Top bound should be 0, but instead is " + sa.getTop(), + sa.getTop() == 0); + } + + public void testSetThenMove() { + SelectionArea sa = new SelectionArea(); + + sa.setBounds(WIDTH, HEIGHT); + + int targetColumn = WIDTH / 2; + int targetRow = HEIGHT / 2; + + sa.setColumn(targetColumn); + sa.setRow(targetRow); + + sa.incrementRow(); + assertTrue("Row should be " + (targetRow + 1) + ", but instead is " + sa.getTop(), + sa.getTop() == (targetRow + 1)); + + sa.decrementColumn(); + assertTrue("Column shold be " + (targetColumn - 1) + ", but instead is " + sa.getLeft(), + sa.getLeft() == (targetColumn - 1)); + + sa.finishSelectingOrigin(); + + sa.setRow(0); + sa.setColumn(0); + + sa.incrementRow(); + sa.decrementColumn(); + + assertTrue("Top row should be 1, but instead is " + sa.getTop(), + sa.getTop() == 1); + + assertTrue("Left column shold be 0, but instead is " + sa.getLeft(), + sa.getLeft() == 0); + + assertTrue("Bottom row should be " + (targetRow + 1) + ", but instead is " + sa.getBottom(), + sa.getBottom() == (targetRow + 1)); + + assertTrue("Right column shold be " + (targetColumn - 1) + ", but instead is " + sa.getRight(), + sa.getRight() == (targetColumn - 1)); + } +} diff --git a/app/src/androidTest/java/org/connectbot/SettingsActivityTest.java b/app/src/androidTest/java/org/connectbot/SettingsActivityTest.java new file mode 100644 index 0000000..6b79136 --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/SettingsActivityTest.java @@ -0,0 +1,47 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import android.test.ActivityInstrumentationTestCase2; + +/** + * This is a simple framework for a test of an Application. See + * {@link android.test.ApplicationTestCase ApplicationTestCase} for more + * information on how to write and extend Application tests. + *

+ * To run this test, you can type: + * adb shell am instrument -w \ + * -e class org.connectbot.HostListActivityTest \ + * org.connectbot.tests/android.test.InstrumentationTestRunner + */ +public class SettingsActivityTest extends + ActivityInstrumentationTestCase2 { + + public SettingsActivityTest() { + super("org.connectbot", SettingsActivity.class); + } + + public void testOpenMenu() { + SettingsActivity a = getActivity(); + + a.openOptionsMenu(); + + a.closeOptionsMenu(); + } + +} diff --git a/app/src/androidTest/java/org/connectbot/TerminalBridgeTest.java b/app/src/androidTest/java/org/connectbot/TerminalBridgeTest.java new file mode 100644 index 0000000..bfa5e23 --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/TerminalBridgeTest.java @@ -0,0 +1,105 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import android.test.AndroidTestCase; + +/** + * @author Kenny Root + * + */ +public class TerminalBridgeTest extends AndroidTestCase { + public void testShiftLock() throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { +// TerminalBridge bridge = new TerminalBridge(); +// AbsTransport nullTransport = new NullTransport(); +// +// // Make sure onKey will work when we call it +// Field disconnected = TerminalBridge.class +// .getDeclaredField("disconnected"); +// Field keymode = TerminalBridge.class.getDeclaredField("keymode"); +// Field transport = TerminalBridge.class.getDeclaredField("transport"); +// +// disconnected.setAccessible(true); +// keymode.setAccessible(true); +// transport.setAccessible(true); +// +// disconnected.setBoolean(bridge, false); +// keymode.set(bridge, PreferenceConstants.KEYMODE_RIGHT); +// transport.set(bridge, nullTransport); +// +// // Begin tests +// assertTrue("Meta state is " + bridge.getMetaState() +// + " when it should be 0", bridge.getMetaState() == 0); +// +// KeyEvent shiftDown = new KeyEvent(KeyEvent.ACTION_DOWN, +// KeyEvent.KEYCODE_SHIFT_LEFT); +// bridge.onKey(null, shiftDown.getKeyCode(), shiftDown); +// +// assertTrue("Shift test: after shift press, meta state is " +// + bridge.getMetaState() + " when it should be " +// + TerminalBridge.META_SHIFT_ON, +// bridge.getMetaState() == TerminalBridge.META_SHIFT_ON); +// +// KeyEvent shiftUp = KeyEvent.changeAction(shiftDown, KeyEvent.ACTION_UP); +// bridge.onKey(null, shiftUp.getKeyCode(), shiftUp); +// +// assertTrue("Shift test: after shift release, meta state is " +// + bridge.getMetaState() + " when it should be " +// + TerminalBridge.META_SHIFT_ON, +// bridge.getMetaState() == TerminalBridge.META_SHIFT_ON); +// +// KeyEvent letterAdown = new KeyEvent(KeyEvent.ACTION_DOWN, +// KeyEvent.KEYCODE_A); +// KeyEvent letterAup = KeyEvent.changeAction(letterAdown, +// KeyEvent.ACTION_UP); +// +// bridge.onKey(null, letterAdown.getKeyCode(), letterAdown); +// bridge.onKey(null, letterAup.getKeyCode(), letterAup); +// +// assertTrue("Shift test: after letter press and release, meta state is " +// + bridge.getMetaState() + " when it should be 0", bridge +// .getMetaState() == 0); +// +// bridge.onKey(null, shiftDown.getKeyCode(), shiftDown); +// bridge.onKey(null, shiftUp.getKeyCode(), shiftUp); +// bridge.onKey(null, shiftDown.getKeyCode(), shiftDown); +// bridge.onKey(null, shiftUp.getKeyCode(), shiftUp); +// +// assertTrue("Shift lock test: after two shift presses, meta state is " +// + bridge.getMetaState() + " when it should be " +// + TerminalBridge.META_SHIFT_LOCK, +// bridge.getMetaState() == TerminalBridge.META_SHIFT_LOCK); +// +// bridge.onKey(null, letterAdown.getKeyCode(), letterAdown); +// +// assertTrue( +// "Shift lock test: after letter press, meta state is " +// + bridge.getMetaState() + " when it should be " +// + TerminalBridge.META_SHIFT_LOCK, +// bridge.getMetaState() == TerminalBridge.META_SHIFT_LOCK); +// +// bridge.onKey(null, letterAup.getKeyCode(), letterAup); +// +// assertTrue( +// "Shift lock test: after letter press and release, meta state is " +// + bridge.getMetaState() + " when it should be " +// + TerminalBridge.META_SHIFT_LOCK, +// bridge.getMetaState() == TerminalBridge.META_SHIFT_LOCK); + } +} diff --git a/app/src/androidTest/java/org/connectbot/mock/BeanTestCase.java b/app/src/androidTest/java/org/connectbot/mock/BeanTestCase.java new file mode 100644 index 0000000..5d13d9f --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/mock/BeanTestCase.java @@ -0,0 +1,229 @@ +/** + * Originally from http://www.cornetdesign.com/files/BeanTestCase.java.txt + */ +package org.connectbot.mock; + +import junit.framework.TestCase; + +import java.lang.reflect.Field; + +public class BeanTestCase extends TestCase { + + private static final String TEST_STRING_VAL1 = "Some Value"; + private static final String TEST_STRING_VAL2 = "Some Other Value"; + + public static void assertMeetsEqualsContract(Class classUnderTest, + String[] fieldNames) { + Object o1; + Object o2; + try { + // Get Instances + o1 = classUnderTest.newInstance(); + o2 = classUnderTest.newInstance(); + + assertTrue( + "Instances with default constructor not equal (o1.equals(o2))", + o1.equals(o2)); + assertTrue( + "Instances with default constructor not equal (o2.equals(o1))", + o2.equals(o1)); + + Field[] fields = getFieldsByNameOrAll(classUnderTest, fieldNames); + + for (int i = 0; i < fields.length; i++) { + + // Reset the instances + o1 = classUnderTest.newInstance(); + o2 = classUnderTest.newInstance(); + + Field field = fields[i]; + field.setAccessible(true); + if (field.getType() == String.class) { + field.set(o1, TEST_STRING_VAL1); + } else if (field.getType() == boolean.class) { + field.setBoolean(o1, true); + } else if (field.getType() == short.class) { + field.setShort(o1, (short) 1); + } else if (field.getType() == long.class) { + field.setLong(o1, (long) 1); + } else if (field.getType() == float.class) { + field.setFloat(o1, (float) 1); + } else if (field.getType() == int.class) { + field.setInt(o1, 1); + } else if (field.getType() == byte.class) { + field.setByte(o1, (byte) 1); + } else if (field.getType() == char.class) { + field.setChar(o1, (char) 1); + } else if (field.getType() == double.class) { + field.setDouble(o1, (double) 1); + } else if (field.getType().isEnum()) { + field.set(o1, field.getType().getEnumConstants()[0]); + } else if (Object.class.isAssignableFrom(field.getType())) { + field.set(o1, field.getType().newInstance()); + } else { + fail("Don't know how to set a " + field.getType().getName()); + } + + assertFalse("Instances with o1 having " + field.getName() + + " set and o2 having it not set are equal", o1 + .equals(o2)); + + field.set(o2, field.get(o1)); + + assertTrue( + "After setting o2 with the value of the object in o1, the two objects in the field are not equal", + field.get(o1).equals(field.get(o2))); + + assertTrue( + "Instances with o1 having " + + field.getName() + + " set and o2 having it set to the same object of type " + + field.get(o2).getClass().getName() + + " are not equal", o1.equals(o2)); + + if (field.getType() == String.class) { + field.set(o2, TEST_STRING_VAL2); + } else if (field.getType() == boolean.class) { + field.setBoolean(o2, false); + } else if (field.getType() == short.class) { + field.setShort(o2, (short) 0); + } else if (field.getType() == long.class) { + field.setLong(o2, (long) 0); + } else if (field.getType() == float.class) { + field.setFloat(o2, (float) 0); + } else if (field.getType() == int.class) { + field.setInt(o2, 0); + } else if (field.getType() == byte.class) { + field.setByte(o2, (byte) 0); + } else if (field.getType() == char.class) { + field.setChar(o2, (char) 0); + } else if (field.getType() == double.class) { + field.setDouble(o2, (double) 1); + } else if (field.getType().isEnum()) { + field.set(o2, field.getType().getEnumConstants()[1]); + } else if (Object.class.isAssignableFrom(field.getType())) { + field.set(o2, field.getType().newInstance()); + } else { + fail("Don't know how to set a " + field.getType().getName()); + } + if (field.get(o1).equals(field.get(o2))) { + // Even though we have different instances, they are equal. + // Let's walk one of them + // to see if we can find a field to set + Field[] paramFields = field.get(o1).getClass() + .getDeclaredFields(); + for (int j = 0; j < paramFields.length; j++) { + paramFields[j].setAccessible(true); + if (paramFields[j].getType() == String.class) { + paramFields[j].set(field.get(o1), TEST_STRING_VAL1); + } + } + } + + assertFalse( + "After setting o2 with a different object than what is in o1, the two objects in the field are equal. " + + "This is after an attempt to walk the fields to make them different", + field.get(o1).equals(field.get(o2))); + assertFalse( + "Instances with o1 having " + + field.getName() + + " set and o2 having it set to a different object are equal", + o1.equals(o2)); + } + + } catch (InstantiationException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to construct an instance of the class under test"); + } catch (IllegalAccessException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to construct an instance of the class under test"); + } catch (SecurityException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to read the field from the class under test"); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to find field in the class under test"); + } + } + + /** + * @param classUnderTest + * @param fieldNames + * @return + * @throws NoSuchFieldException + */ + private static Field[] getFieldsByNameOrAll(Class classUnderTest, + String[] fieldNames) throws NoSuchFieldException { + Field fields[]; + if (fieldNames == null) { + fields = classUnderTest.getDeclaredFields(); + } else { + fields = new Field[fieldNames.length]; + for (int i = 0; i < fieldNames.length; i++) + fields[i] = classUnderTest.getDeclaredField(fieldNames[i]); + } + return fields; + } + + public static void assertMeetsHashCodeContract(Class classUnderTest, + String[] fieldNames) { + try { + Field[] fields = getFieldsByNameOrAll(classUnderTest, fieldNames); + + for (int i = 0; i < fields.length; i++) { + Object o1 = classUnderTest.newInstance(); + int initialHashCode = o1.hashCode(); + + Field field = fields[i]; + field.setAccessible(true); + if (field.getType() == String.class) { + field.set(o1, TEST_STRING_VAL1); + } else if (field.getType() == boolean.class) { + field.setBoolean(o1, true); + } else if (field.getType() == short.class) { + field.setShort(o1, (short) 1); + } else if (field.getType() == long.class) { + field.setLong(o1, (long) 1); + } else if (field.getType() == float.class) { + field.setFloat(o1, (float) 1); + } else if (field.getType() == int.class) { + field.setInt(o1, 1); + } else if (field.getType() == byte.class) { + field.setByte(o1, (byte) 1); + } else if (field.getType() == char.class) { + field.setChar(o1, (char) 1); + } else if (field.getType() == double.class) { + field.setDouble(o1, (double) 1); + } else if (field.getType().isEnum()) { + field.set(o1, field.getType().getEnumConstants()[0]); + } else if (Object.class.isAssignableFrom(field.getType())) { + field.set(o1, field.getType().newInstance()); + } else { + fail("Don't know how to set a " + field.getType().getName()); + } + int updatedHashCode = o1.hashCode(); + assertFalse( + "The field " + + field.getName() + + " was not taken into account for the hashCode contract ", + initialHashCode == updatedHashCode); + } + } catch (InstantiationException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to construct an instance of the class under test"); + } catch (IllegalAccessException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to construct an instance of the class under test"); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + throw new AssertionError( + "Unable to find field in the class under test"); + } + } +} diff --git a/app/src/androidTest/java/org/connectbot/mock/NullOutputStream.java b/app/src/androidTest/java/org/connectbot/mock/NullOutputStream.java new file mode 100644 index 0000000..79b8e72 --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/mock/NullOutputStream.java @@ -0,0 +1,33 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.mock; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * @author Kenny Root + * + */ +public class NullOutputStream extends OutputStream { + @Override + public void write(int arg0) throws IOException { + // do nothing + } + +} diff --git a/app/src/androidTest/java/org/connectbot/mock/NullTransport.java b/app/src/androidTest/java/org/connectbot/mock/NullTransport.java new file mode 100644 index 0000000..d841e6a --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/mock/NullTransport.java @@ -0,0 +1,138 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.mock; + +import java.io.IOException; +import java.util.Map; + +import org.connectbot.bean.HostBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.transport.AbsTransport; + +import android.net.Uri; + +/** + * @author kenny + * + */ +public class NullTransport extends AbsTransport { + + /** + * + */ + public NullTransport() { + // TODO Auto-generated constructor stub + } + + /** + * @param host + * @param bridge + * @param manager + */ + public NullTransport(HostBean host, TerminalBridge bridge, + TerminalManager manager) { + super(host, bridge, manager); + // TODO Auto-generated constructor stub + } + + @Override + public void close() { + // TODO Auto-generated method stub + + } + + @Override + public void connect() { + // TODO Auto-generated method stub + + } + + @Override + public HostBean createHost(Uri uri) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void flush() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public String getDefaultNickname(String username, String hostname, int port) { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getDefaultPort() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void getSelectionArgs(Uri uri, Map selection) { + // TODO Auto-generated method stub + + } + + @Override + public boolean isConnected() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isSessionOpen() { + // TODO Auto-generated method stub + return false; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void setDimensions(int columns, int rows, int width, int height) { + // TODO Auto-generated method stub + + } + + @Override + public void write(byte[] buffer) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void write(int c) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public boolean usesNetwork() { + // TODO Auto-generated method stub + return false; + } + +} diff --git a/app/src/androidTest/java/org/connectbot/util/PubkeyUtilsTest.java b/app/src/androidTest/java/org/connectbot/util/PubkeyUtilsTest.java new file mode 100644 index 0000000..1eb1ee6 --- /dev/null +++ b/app/src/androidTest/java/org/connectbot/util/PubkeyUtilsTest.java @@ -0,0 +1,345 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; + +import android.test.AndroidTestCase; + +/** + * @author Kenny Root + * + */ +public class PubkeyUtilsTest extends AndroidTestCase { + public void testEncodeHex_Null_Failure() throws Exception { + try { + PubkeyUtils.encodeHex(null); + fail("Should throw null pointer exception when argument is null"); + } catch (NullPointerException e) { + // success + } + } + public void testEncodeHex_Success() throws Exception { + byte[] input = {(byte) 0xFF, 0x00, (byte) 0xA5, 0x5A, 0x12, 0x23}; + String expected = "ff00a55a1223"; + + assertEquals("Encoded hex should match expected", + PubkeyUtils.encodeHex(input), expected); + } + + public void testSha256_Empty_Success() throws Exception { + byte[] empty_hashed = new byte[] { + (byte) 0xe3, (byte) 0xb0, (byte) 0xc4, (byte) 0x42, + (byte) 0x98, (byte) 0xfc, (byte) 0x1c, (byte) 0x14, + (byte) 0x9a, (byte) 0xfb, (byte) 0xf4, (byte) 0xc8, + (byte) 0x99, (byte) 0x6f, (byte) 0xb9, (byte) 0x24, + (byte) 0x27, (byte) 0xae, (byte) 0x41, (byte) 0xe4, + (byte) 0x64, (byte) 0x9b, (byte) 0x93, (byte) 0x4c, + (byte) 0xa4, (byte) 0x95, (byte) 0x99, (byte) 0x1b, + (byte) 0x78, (byte) 0x52, (byte) 0xb8, (byte) 0x55, + }; + + final byte[] empty = new byte[] {}; + + assertTrue("Empty string should be equal to known test vector", + Arrays.equals(empty_hashed, PubkeyUtils.sha256(empty))); + } + + /* openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -outform d -nocrypt | recode ../x1 | sed 's/0x/(byte) 0x/g' */ + private static final byte[] EC_KEY_PKCS8 = new byte[] { (byte) 0x30, (byte) 0x81, (byte) 0x87, + (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x30, (byte) 0x13, (byte) 0x06, + (byte) 0x07, (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0xCE, (byte) 0x3D, + (byte) 0x02, (byte) 0x01, (byte) 0x06, (byte) 0x08, (byte) 0x2A, (byte) 0x86, + (byte) 0x48, (byte) 0xCE, (byte) 0x3D, (byte) 0x03, (byte) 0x01, (byte) 0x07, + (byte) 0x04, (byte) 0x6D, (byte) 0x30, (byte) 0x6B, (byte) 0x02, (byte) 0x01, + (byte) 0x01, (byte) 0x04, (byte) 0x20, (byte) 0xC7, (byte) 0x6B, (byte) 0xA5, + (byte) 0xB6, (byte) 0xB7, (byte) 0x4E, (byte) 0x0B, (byte) 0x70, (byte) 0x2E, + (byte) 0xA0, (byte) 0x5D, (byte) 0x8D, (byte) 0x0A, (byte) 0xF5, (byte) 0x43, + (byte) 0xEF, (byte) 0x54, (byte) 0x2F, (byte) 0x05, (byte) 0x5B, (byte) 0x66, + (byte) 0x50, (byte) 0xC5, (byte) 0xB4, (byte) 0xA8, (byte) 0x60, (byte) 0x16, + (byte) 0x8E, (byte) 0x8D, (byte) 0xCD, (byte) 0x11, (byte) 0xFA, (byte) 0xA1, + (byte) 0x44, (byte) 0x03, (byte) 0x42, (byte) 0x00, (byte) 0x04, (byte) 0x12, + (byte) 0xE2, (byte) 0x70, (byte) 0x30, (byte) 0x87, (byte) 0x2F, (byte) 0xDE, + (byte) 0x10, (byte) 0xD9, (byte) 0xC9, (byte) 0x83, (byte) 0xC7, (byte) 0x8D, + (byte) 0xC9, (byte) 0x9B, (byte) 0x94, (byte) 0x24, (byte) 0x50, (byte) 0x5D, + (byte) 0xEC, (byte) 0xF1, (byte) 0x4F, (byte) 0x52, (byte) 0xC6, (byte) 0xE7, + (byte) 0xA3, (byte) 0xD7, (byte) 0xF4, (byte) 0x7C, (byte) 0x09, (byte) 0xA1, + (byte) 0x10, (byte) 0x11, (byte) 0xE4, (byte) 0x9E, (byte) 0x90, (byte) 0xAF, + (byte) 0xF9, (byte) 0x4A, (byte) 0x74, (byte) 0x09, (byte) 0x93, (byte) 0xC7, + (byte) 0x9A, (byte) 0xB3, (byte) 0xE2, (byte) 0xD8, (byte) 0x61, (byte) 0x5F, + (byte) 0x86, (byte) 0x14, (byte) 0x91, (byte) 0x7A, (byte) 0x23, (byte) 0x81, + (byte) 0x42, (byte) 0xA9, (byte) 0x02, (byte) 0x1D, (byte) 0x33, (byte) 0x19, + (byte) 0xC0, (byte) 0x4B, (byte) 0xCE + }; + + private static final BigInteger EC_KEY_priv = new BigInteger("c76ba5b6b74e0b702ea05d8d0af543ef542f055b6650c5b4a860168e8dcd11fa", 16); + private static final BigInteger EC_KEY_pub_x = new BigInteger("12e27030872fde10d9c983c78dc99b9424505decf14f52c6e7a3d7f47c09a110", 16); + private static final BigInteger EC_KEY_pub_y = new BigInteger("11e49e90aff94a740993c79ab3e2d8615f8614917a238142a9021d3319c04bce", 16); + + /* openssl genrsa 512 | openssl pkcs8 -topk8 -outform d -nocrypt | recode ../x1 | sed 's/0x/(byte) 0x/g' */ + private static final byte[] RSA_KEY_PKCS8 = new byte[] { (byte) 0x30, (byte) 0x82, (byte) 0x01, + (byte) 0x55, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x30, (byte) 0x0D, + (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0x86, + (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, + (byte) 0x00, (byte) 0x04, (byte) 0x82, (byte) 0x01, (byte) 0x3F, (byte) 0x30, + (byte) 0x82, (byte) 0x01, (byte) 0x3B, (byte) 0x02, (byte) 0x01, (byte) 0x00, + (byte) 0x02, (byte) 0x41, (byte) 0x00, (byte) 0xC6, (byte) 0x00, (byte) 0x79, + (byte) 0x0C, (byte) 0x46, (byte) 0xF9, (byte) 0x03, (byte) 0x15, (byte) 0xBA, + (byte) 0x35, (byte) 0x63, (byte) 0x6C, (byte) 0x97, (byte) 0x3A, (byte) 0x6C, + (byte) 0xC8, (byte) 0x15, (byte) 0x32, (byte) 0x2A, (byte) 0x62, (byte) 0x72, + (byte) 0xBD, (byte) 0x05, (byte) 0x01, (byte) 0xCF, (byte) 0xE6, (byte) 0x49, + (byte) 0xEC, (byte) 0xC9, (byte) 0x8A, (byte) 0x3A, (byte) 0x4E, (byte) 0xB1, + (byte) 0xF2, (byte) 0x3E, (byte) 0x86, (byte) 0x3C, (byte) 0x64, (byte) 0x4A, + (byte) 0x0A, (byte) 0x29, (byte) 0xD6, (byte) 0xFA, (byte) 0xF9, (byte) 0xAC, + (byte) 0xD8, (byte) 0x7B, (byte) 0x9F, (byte) 0x2A, (byte) 0x6B, (byte) 0x13, + (byte) 0x06, (byte) 0x06, (byte) 0xEB, (byte) 0x83, (byte) 0x1B, (byte) 0xB8, + (byte) 0x97, (byte) 0xA3, (byte) 0x91, (byte) 0x95, (byte) 0x60, (byte) 0x15, + (byte) 0xE5, (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, + (byte) 0x02, (byte) 0x40, (byte) 0x0F, (byte) 0xDA, (byte) 0x33, (byte) 0xD6, + (byte) 0xCE, (byte) 0xCB, (byte) 0xDA, (byte) 0xFA, (byte) 0x5F, (byte) 0x59, + (byte) 0x2C, (byte) 0xE7, (byte) 0xA1, (byte) 0xC7, (byte) 0xF4, (byte) 0xB3, + (byte) 0xA4, (byte) 0x36, (byte) 0xCA, (byte) 0xFB, (byte) 0xEC, (byte) 0xD1, + (byte) 0xC3, (byte) 0x57, (byte) 0xDC, (byte) 0xCC, (byte) 0x44, (byte) 0x38, + (byte) 0xE7, (byte) 0xFD, (byte) 0xE0, (byte) 0x23, (byte) 0x0E, (byte) 0x97, + (byte) 0x87, (byte) 0x55, (byte) 0x80, (byte) 0x2B, (byte) 0xF2, (byte) 0xF4, + (byte) 0x1C, (byte) 0x03, (byte) 0xD2, (byte) 0x3E, (byte) 0x09, (byte) 0x72, + (byte) 0x49, (byte) 0xD8, (byte) 0x9C, (byte) 0xAC, (byte) 0xDA, (byte) 0x65, + (byte) 0x68, (byte) 0x4D, (byte) 0x38, (byte) 0x19, (byte) 0xD8, (byte) 0xB1, + (byte) 0x5B, (byte) 0xB7, (byte) 0x38, (byte) 0xC8, (byte) 0x94, (byte) 0xB5, + (byte) 0x02, (byte) 0x21, (byte) 0x00, (byte) 0xF7, (byte) 0x8E, (byte) 0x20, + (byte) 0xDC, (byte) 0x26, (byte) 0x12, (byte) 0x3A, (byte) 0x85, (byte) 0x91, + (byte) 0x5F, (byte) 0x45, (byte) 0xA6, (byte) 0x95, (byte) 0xE5, (byte) 0x22, + (byte) 0xD0, (byte) 0xC4, (byte) 0xD7, (byte) 0x6A, (byte) 0xF1, (byte) 0x43, + (byte) 0x38, (byte) 0x88, (byte) 0x20, (byte) 0x7D, (byte) 0x80, (byte) 0x73, + (byte) 0x7B, (byte) 0xDC, (byte) 0x73, (byte) 0x51, (byte) 0x3B, (byte) 0x02, + (byte) 0x21, (byte) 0x00, (byte) 0xCC, (byte) 0xC1, (byte) 0x99, (byte) 0xC8, + (byte) 0xC0, (byte) 0x54, (byte) 0xBC, (byte) 0xE9, (byte) 0xFB, (byte) 0x77, + (byte) 0x28, (byte) 0xB8, (byte) 0x26, (byte) 0x02, (byte) 0xC0, (byte) 0x0C, + (byte) 0xDE, (byte) 0xFD, (byte) 0xEA, (byte) 0xD0, (byte) 0x15, (byte) 0x4B, + (byte) 0x3B, (byte) 0xD1, (byte) 0xDD, (byte) 0xFD, (byte) 0x5B, (byte) 0xAC, + (byte) 0xB3, (byte) 0xCF, (byte) 0xC3, (byte) 0x5F, (byte) 0x02, (byte) 0x21, + (byte) 0x00, (byte) 0xCD, (byte) 0x8C, (byte) 0x25, (byte) 0x9C, (byte) 0xA5, + (byte) 0xBF, (byte) 0xDC, (byte) 0xF7, (byte) 0xAA, (byte) 0x8D, (byte) 0x00, + (byte) 0xB8, (byte) 0x21, (byte) 0x1D, (byte) 0xF0, (byte) 0x9A, (byte) 0x87, + (byte) 0xD6, (byte) 0x95, (byte) 0xE5, (byte) 0x5D, (byte) 0x7B, (byte) 0x43, + (byte) 0x0C, (byte) 0x37, (byte) 0x28, (byte) 0xC0, (byte) 0xBA, (byte) 0xC7, + (byte) 0x80, (byte) 0xB8, (byte) 0xA1, (byte) 0x02, (byte) 0x21, (byte) 0x00, + (byte) 0xCC, (byte) 0x26, (byte) 0x6F, (byte) 0xAD, (byte) 0x60, (byte) 0x4E, + (byte) 0x5C, (byte) 0xB9, (byte) 0x32, (byte) 0x57, (byte) 0x61, (byte) 0x8B, + (byte) 0x11, (byte) 0xA3, (byte) 0x06, (byte) 0x57, (byte) 0x0E, (byte) 0xF2, + (byte) 0xBE, (byte) 0x6F, (byte) 0x4F, (byte) 0xFB, (byte) 0xDE, (byte) 0x1D, + (byte) 0xE6, (byte) 0xA7, (byte) 0x19, (byte) 0x03, (byte) 0x7D, (byte) 0x98, + (byte) 0xB6, (byte) 0x23, (byte) 0x02, (byte) 0x20, (byte) 0x24, (byte) 0x80, + (byte) 0x94, (byte) 0xFF, (byte) 0xDD, (byte) 0x7A, (byte) 0x22, (byte) 0x7D, + (byte) 0xC4, (byte) 0x5A, (byte) 0xFD, (byte) 0x84, (byte) 0xC1, (byte) 0xAD, + (byte) 0x8A, (byte) 0x13, (byte) 0x2A, (byte) 0xF9, (byte) 0x5D, (byte) 0xFF, + (byte) 0x0B, (byte) 0x2E, (byte) 0x0F, (byte) 0x61, (byte) 0x42, (byte) 0x88, + (byte) 0x57, (byte) 0xCF, (byte) 0xC1, (byte) 0x71, (byte) 0xC9, (byte) 0xB9 + }; + + private static final BigInteger RSA_KEY_N = new BigInteger("C600790C46F90315BA35636C973A6CC815322A6272BD0501CFE649ECC98A3A4EB1F23E863C644A0A29D6FAF9ACD87B9F2A6B130606EB831BB897A391956015E5", 16); + private static final BigInteger RSA_KEY_E = new BigInteger("010001", 16); + + /* + openssl dsaparam -genkey -text -out dsakey.pem 1024 + openssl dsa -in dsakey.pem -text -noout + openssl pkcs8 -topk8 -in dsakey.pem -outform d -nocrypt | recode ../x1 | sed 's/0x/(byte) 0x/g' + */ + private static final byte[] DSA_KEY_PKCS8 = new byte[] { + (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x4A, (byte) 0x02, (byte) 0x01, + (byte) 0x00, (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x2B, (byte) 0x06, + (byte) 0x07, (byte) 0x2A, (byte) 0x86, (byte) 0x48, (byte) 0xCE, (byte) 0x38, + (byte) 0x04, (byte) 0x01, (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x1E, + (byte) 0x02, (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xD2, (byte) 0x18, + (byte) 0xDB, (byte) 0x94, (byte) 0x7C, (byte) 0xD6, (byte) 0x2E, (byte) 0xE2, + (byte) 0x07, (byte) 0x38, (byte) 0x42, (byte) 0xC4, (byte) 0x16, (byte) 0x24, + (byte) 0x94, (byte) 0x2F, (byte) 0xC1, (byte) 0x0F, (byte) 0x92, (byte) 0x0A, + (byte) 0x44, (byte) 0x44, (byte) 0x99, (byte) 0xFC, (byte) 0x01, (byte) 0x1B, + (byte) 0xF8, (byte) 0xF3, (byte) 0x82, (byte) 0x57, (byte) 0x01, (byte) 0x8D, + (byte) 0xE6, (byte) 0x22, (byte) 0x70, (byte) 0xA0, (byte) 0xD6, (byte) 0x05, + (byte) 0x0F, (byte) 0xF1, (byte) 0xD0, (byte) 0xF4, (byte) 0x0B, (byte) 0xA2, + (byte) 0xE4, (byte) 0x1E, (byte) 0xD3, (byte) 0x44, (byte) 0x79, (byte) 0x74, + (byte) 0x4C, (byte) 0xC1, (byte) 0xA7, (byte) 0xA5, (byte) 0x84, (byte) 0xD8, + (byte) 0xB9, (byte) 0xDF, (byte) 0xA3, (byte) 0x85, (byte) 0xFA, (byte) 0xF2, + (byte) 0xFD, (byte) 0x44, (byte) 0x0B, (byte) 0xB1, (byte) 0xA5, (byte) 0x82, + (byte) 0x8D, (byte) 0x06, (byte) 0x92, (byte) 0xCA, (byte) 0xB4, (byte) 0xFB, + (byte) 0xDF, (byte) 0xC2, (byte) 0xFD, (byte) 0xA7, (byte) 0xCB, (byte) 0x6F, + (byte) 0x03, (byte) 0xB9, (byte) 0xEF, (byte) 0xFD, (byte) 0x7F, (byte) 0xBC, + (byte) 0xB3, (byte) 0x1D, (byte) 0xA4, (byte) 0xE8, (byte) 0x7D, (byte) 0xA2, + (byte) 0xCF, (byte) 0x62, (byte) 0x35, (byte) 0x06, (byte) 0xC8, (byte) 0xFE, + (byte) 0xE6, (byte) 0xE7, (byte) 0x6E, (byte) 0xAE, (byte) 0x22, (byte) 0xE7, + (byte) 0x82, (byte) 0x38, (byte) 0x54, (byte) 0x82, (byte) 0xCD, (byte) 0xEA, + (byte) 0xD8, (byte) 0x69, (byte) 0xBB, (byte) 0x1C, (byte) 0xD3, (byte) 0x70, + (byte) 0x32, (byte) 0xB1, (byte) 0xFB, (byte) 0x07, (byte) 0x01, (byte) 0x66, + (byte) 0xCC, (byte) 0x24, (byte) 0xD6, (byte) 0x50, (byte) 0x46, (byte) 0x9B, + (byte) 0x02, (byte) 0x15, (byte) 0x00, (byte) 0xD6, (byte) 0xE6, (byte) 0x7E, + (byte) 0x1A, (byte) 0xE5, (byte) 0xCA, (byte) 0x1D, (byte) 0xB6, (byte) 0xAF, + (byte) 0x4E, (byte) 0xD9, (byte) 0x18, (byte) 0xE8, (byte) 0x87, (byte) 0xB1, + (byte) 0xBC, (byte) 0x93, (byte) 0xE1, (byte) 0x80, (byte) 0xF5, (byte) 0x02, + (byte) 0x81, (byte) 0x80, (byte) 0x19, (byte) 0x20, (byte) 0xCC, (byte) 0x18, + (byte) 0xF6, (byte) 0x8F, (byte) 0x73, (byte) 0xFA, (byte) 0x9F, (byte) 0x50, + (byte) 0xC8, (byte) 0x92, (byte) 0xBE, (byte) 0x07, (byte) 0x7C, (byte) 0x34, + (byte) 0xD8, (byte) 0x6F, (byte) 0x63, (byte) 0xC9, (byte) 0x35, (byte) 0x48, + (byte) 0x79, (byte) 0x79, (byte) 0x26, (byte) 0xEF, (byte) 0x1E, (byte) 0x99, + (byte) 0x54, (byte) 0xD7, (byte) 0x30, (byte) 0x2C, (byte) 0x68, (byte) 0xBC, + (byte) 0xFF, (byte) 0xF2, (byte) 0x4C, (byte) 0x6A, (byte) 0xD3, (byte) 0x2D, + (byte) 0x1C, (byte) 0x7A, (byte) 0x06, (byte) 0x11, (byte) 0x72, (byte) 0x92, + (byte) 0x9C, (byte) 0xAA, (byte) 0x95, (byte) 0x0E, (byte) 0x44, (byte) 0x2C, + (byte) 0x5F, (byte) 0x19, (byte) 0x25, (byte) 0xB4, (byte) 0xBF, (byte) 0x21, + (byte) 0x8F, (byte) 0xB7, (byte) 0x7E, (byte) 0x4B, (byte) 0x64, (byte) 0x83, + (byte) 0x59, (byte) 0x20, (byte) 0x20, (byte) 0x36, (byte) 0x84, (byte) 0xA4, + (byte) 0x1D, (byte) 0xB5, (byte) 0xCA, (byte) 0x7F, (byte) 0x10, (byte) 0x4E, + (byte) 0x27, (byte) 0x21, (byte) 0x8E, (byte) 0x2C, (byte) 0xA5, (byte) 0xF8, + (byte) 0xAC, (byte) 0xBD, (byte) 0xF5, (byte) 0xB5, (byte) 0xBA, (byte) 0xEB, + (byte) 0x86, (byte) 0x6F, (byte) 0x7F, (byte) 0xB1, (byte) 0xE0, (byte) 0x90, + (byte) 0x35, (byte) 0xCA, (byte) 0xA8, (byte) 0x64, (byte) 0x6E, (byte) 0x06, + (byte) 0x3D, (byte) 0x02, (byte) 0x3D, (byte) 0x95, (byte) 0x57, (byte) 0xB3, + (byte) 0x8A, (byte) 0xE2, (byte) 0x0B, (byte) 0xD3, (byte) 0x9E, (byte) 0x1C, + (byte) 0x13, (byte) 0xDE, (byte) 0x48, (byte) 0xA3, (byte) 0xC2, (byte) 0x11, + (byte) 0xDA, (byte) 0x75, (byte) 0x09, (byte) 0xF6, (byte) 0x92, (byte) 0x0F, + (byte) 0x0F, (byte) 0xA6, (byte) 0xF3, (byte) 0x3E, (byte) 0x04, (byte) 0x16, + (byte) 0x02, (byte) 0x14, (byte) 0x29, (byte) 0x50, (byte) 0xE4, (byte) 0x77, + (byte) 0x4F, (byte) 0xB2, (byte) 0xFF, (byte) 0xFB, (byte) 0x5D, (byte) 0x33, + (byte) 0xC9, (byte) 0x37, (byte) 0xF0, (byte) 0xB5, (byte) 0x8F, (byte) 0xFB, + (byte) 0x0D, (byte) 0x45, (byte) 0xC2, (byte) 0x00 + }; + + private static final BigInteger DSA_KEY_P = new BigInteger("00d218db947cd62ee2073842c41624942fc10f920a444499fc011bf8f38257018de62270a0d6050ff1d0f40ba2e41ed34479744cc1a7a584d8b9dfa385faf2fd440bb1a5828d0692cab4fbdfc2fda7cb6f03b9effd7fbcb31da4e87da2cf623506c8fee6e76eae22e782385482cdead869bb1cd37032b1fb070166cc24d650469b", 16); + private static final BigInteger DSA_KEY_Q = new BigInteger("00d6e67e1ae5ca1db6af4ed918e887b1bc93e180f5", 16); + private static final BigInteger DSA_KEY_G = new BigInteger("1920cc18f68f73fa9f50c892be077c34d86f63c93548797926ef1e9954d7302c68bcfff24c6ad32d1c7a061172929caa950e442c5f1925b4bf218fb77e4b64835920203684a41db5ca7f104e27218e2ca5f8acbdf5b5baeb866f7fb1e09035caa8646e063d023d9557b38ae20bd39e1c13de48a3c211da7509f6920f0fa6f33e", 16); + + private static final BigInteger DSA_KEY_priv = new BigInteger("2950e4774fb2fffb5d33c937f0b58ffb0d45c200", 16); + private static final BigInteger DSA_KEY_pub = new BigInteger("0087b82cdf3232db3bec0d00e96c8393bc7f5629551ea1a00888961cf56e80a36f2a7b316bc10b1d367a5ea374235c9361a472a9176f6cf61f708b86a52b4fae814abd1f1bdd16eea94aea9281851032b1bad7567624c615d6899ca1c94ad614f14e767e49d2ba5223cd113a0d02b66183653cd346ae76d85843afe66520904274", 16); + + public void testGetOidFromPkcs8Encoded_Ec_NistP256() throws Exception { + assertEquals("1.2.840.10045.2.1", PubkeyUtils.getOidFromPkcs8Encoded(EC_KEY_PKCS8)); + } + + public void testGetOidFromPkcs8Encoded_Rsa() throws Exception { + assertEquals("1.2.840.113549.1.1.1", PubkeyUtils.getOidFromPkcs8Encoded(RSA_KEY_PKCS8)); + } + + public void testGetOidFromPkcs8Encoded_Dsa() throws Exception { + assertEquals("1.2.840.10040.4.1", PubkeyUtils.getOidFromPkcs8Encoded(DSA_KEY_PKCS8)); + } + + public void testGetOidFromPkcs8Encoded_Null_Failure() throws Exception { + try { + PubkeyUtils.getOidFromPkcs8Encoded(null); + fail("Should throw NoSuchAlgorithmException"); + } catch (NoSuchAlgorithmException expected) { + } + } + + public void testGetOidFromPkcs8Encoded_NotCorrectDer_Failure() throws Exception { + try { + PubkeyUtils.getOidFromPkcs8Encoded(new byte[] { 0x30, 0x01, 0x00 }); + fail("Should throw NoSuchAlgorithmException"); + } catch (NoSuchAlgorithmException expected) { + } + } + + public void testGetAlgorithmForOid_Ecdsa() throws Exception { + assertEquals("EC", PubkeyUtils.getAlgorithmForOid("1.2.840.10045.2.1")); + } + + public void testGetAlgorithmForOid_Rsa() throws Exception { + assertEquals("RSA", PubkeyUtils.getAlgorithmForOid("1.2.840.113549.1.1.1")); + } + + public void testGetAlgorithmForOid_Dsa() throws Exception { + assertEquals("DSA", PubkeyUtils.getAlgorithmForOid("1.2.840.10040.4.1")); + } + + public void testGetAlgorithmForOid_NullInput_Failure() throws Exception { + try { + PubkeyUtils.getAlgorithmForOid(null); + fail("Should throw NoSuchAlgorithmException"); + } catch (NoSuchAlgorithmException expected) { + } + } + + public void testGetAlgorithmForOid_UnknownOid_Failure() throws Exception { + try { + PubkeyUtils.getAlgorithmForOid("1.3.66666.2000.4000.1"); + fail("Should throw NoSuchAlgorithmException"); + } catch (NoSuchAlgorithmException expected) { + } + } + + public void testRecoverKeyPair_Dsa() throws Exception { + KeyPair kp = PubkeyUtils.recoverKeyPair(DSA_KEY_PKCS8); + + DSAPublicKey pubKey = (DSAPublicKey) kp.getPublic(); + + assertEquals(DSA_KEY_pub, pubKey.getY()); + + DSAParams params = pubKey.getParams(); + assertEquals(params.getG(), DSA_KEY_G); + assertEquals(params.getP(), DSA_KEY_P); + assertEquals(params.getQ(), DSA_KEY_Q); + } + + public void testRecoverKeyPair_Rsa() throws Exception { + KeyPair kp = PubkeyUtils.recoverKeyPair(RSA_KEY_PKCS8); + + RSAPublicKey pubKey = (RSAPublicKey) kp.getPublic(); + + assertEquals(RSA_KEY_N, pubKey.getModulus()); + assertEquals(RSA_KEY_E, pubKey.getPublicExponent()); + } + + public void testRecoverKeyPair_Ec() throws Exception { + KeyPair kp = PubkeyUtils.recoverKeyPair(EC_KEY_PKCS8); + + ECPublicKey pubKey = (ECPublicKey) kp.getPublic(); + + assertEquals(EC_KEY_pub_x, pubKey.getW().getAffineX()); + assertEquals(EC_KEY_pub_y, pubKey.getW().getAffineY()); + } + + private static class MyPrivateKey implements PrivateKey { + public String getAlgorithm() { + throw new UnsupportedOperationException(); + } + + public byte[] getEncoded() { + throw new UnsupportedOperationException(); + } + + public String getFormat() { + throw new UnsupportedOperationException(); + } + } + + public void testRecoverPublicKey_FakeKey_Failure() throws Exception { + try { + PubkeyUtils.recoverPublicKey(null, new MyPrivateKey()); + fail("Should not accept unknown key types"); + } catch (NoSuchAlgorithmException expected) { + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..87e7e18 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/help/Hints.html b/app/src/main/assets/help/Hints.html new file mode 100644 index 0000000..37583db --- /dev/null +++ b/app/src/main/assets/help/Hints.html @@ -0,0 +1,13 @@ + + + +

Helpful hints

+ +

When you have multiple sessions open, you can 'pan' between them by swiping your finger left-to-right or right-to-left over the screen.

+ +

Long-press on your Android desktop to create direct shortcuts to frequently-used SSH hosts.

+ +

Slide your finger up/down on the right-half of the terminal screen to look at the scrollback history. Slide up/down on the left-half to send the page up/down keys.

+ + + \ No newline at end of file diff --git a/app/src/main/assets/help/PhysicalKeyboard.html b/app/src/main/assets/help/PhysicalKeyboard.html new file mode 100644 index 0000000..4ff3753 --- /dev/null +++ b/app/src/main/assets/help/PhysicalKeyboard.html @@ -0,0 +1,62 @@ + + + +

+

Here are some keyboard shortcuts available when a hardware +keyboard is present. If you're using a phone where the main input +type is a virtual keyboard, please see the VirtualKeyboard help topic. +

+

Note: the side that shift, alt, +slash, and tab uses can be changed in +preferences between left, right, and disabled.

+
    +
  • Control key (CTRL)
  • +
    Pressing once on the trackball will toggle on + control for the next character typed. The cursor will + indicate this state with a < symbol. Note that pressing the + trackball again will send an escape key.
    +
+
    +
  • Escape (ESC)
  • +
    Pressing twice on the trackball will send escape + key. Note that some other terminal emulators map pressing ALT-key + to escape + key.
    +
+
    +
  • Shift
  • +
    Pressing the shift (up arrow) + key once will make the next key typed its uppercase variant according + to the keyboard layout. This state is indicated with an outline of a + triangle on the top of the cursor. Pressing it twice will turn on shift + lock which is indicated by a solid triangle on the top of the cursor.
    +
+
    +
  • Alt
  • +
    Pressing the Alt key once + will make the next key typed its symbol as indicated on the keyboard. + This state is indicated with the outline of a triangle on the bottom of + the cursor. Pressing it twice will turn on alt lock + which is indicated by a solid triangle on the bottom of the cursor.
    +
+
    +
  • Slash (opposite side Alt)
  • +
    The opposite side alt key can + be used as a shortcut for the forward slash / character. This aids in + quickly typing directories on the G1.
    +
+
    +
  • Tab (opposite side Shift)
  • +
    The opposite side shift key + can be used as a shortcut for the tab key (CTRL-i) for + quick completion in many shells.
    +
+
    +
  • Function keys (F1 through F10)
  • +
    Hold down the shift key and press numbers 1 + through 10 to send F1 through F10 respectively.
    +
+ + + + diff --git a/app/src/main/assets/help/ScreenGestures.html b/app/src/main/assets/help/ScreenGestures.html new file mode 100644 index 0000000..9332821 --- /dev/null +++ b/app/src/main/assets/help/ScreenGestures.html @@ -0,0 +1,38 @@ + + + +

Gestures in ConnectBot allow a user to do several things for +which there's no keyboard equivalent. If the gestures seem +backward, then imagine that you're grabbing the text and moving it +with your finger.

+

Page Up / Page Down

+

Swiping your finger up and down on the left third of the screen +will send a page up and page down key to the remote host. Many programs +map this to scrolling back into history such as irssi or tinyfugue.

+

+Page Up gesture

+

+Page Down gesture

+

Scroll back / Scroll +forward

+

Swiping your finger up on the right side of the screen allows you +to scroll backward and forward in the local terminal buffer history.

+

+Scroll back gesture

+

+Scroll forward gesture

+

Switching hosts

+

Swiping your finger from one side of the screen to the other will +switch between currently connected hosts.

+

+Previous host gesture

+

+Next host gesture

+ + \ No newline at end of file diff --git a/app/src/main/assets/help/VirtualKeyboard.html b/app/src/main/assets/help/VirtualKeyboard.html new file mode 100644 index 0000000..9788161 --- /dev/null +++ b/app/src/main/assets/help/VirtualKeyboard.html @@ -0,0 +1,49 @@ + + + +

+ +

Caveats

+

Since ConnectBot doesn't use any of the normal TextView +widgets, Android's IME structure isn't designed to directly +support it.

+

The best way to use Android with a virtual keyboard is in Portrait +mode. By default, ConnectBot is set to use Portrait +mode when no hardware keyboard is present. To change this setting, go to +Preferences from the Host List.

+

In Landscape mode, the Android virtual keyboard +(or other IMEs) will take up the entire screen. Android provides no way +for ConnectBot to resize the terminal view in Landscape. +However, you may use a work-around: Force Resize +to fit above the virtual keyboard if desired.

+

On devices without a hardware keyboard, you may press and hold +the MENU button to bring up the virtual keyboard. NOTE: +This applies to any program on the Android platform; it is not +ConnectBot specific.

+

How to Enter Control, Alt, Escape, and Function Keys

+

You can enter any key combination with ConnectBot and the virtual +keyboard, but you must know how keys are mapped on a normal console. For +instance, usually combinations of ALT+letter on a PC keyboard are +actually mapped to sending, sequentially, ESC key then the letter.

+

Note there are also screen gestures: see the ScreenGestures help topic. +for Page Up and Page Down.

+
    +
  • Trackball: 1 press is CTRL, 2 presses sends ESC +
  • +
  • Tab key = CTRL + i
  • +
  • Function key = CTRL + number +
  • +
+

Examples

+
    +
  • ESC = Press the trackball twice.
  • +
  • ALT + Right Arrow = Press trackball twice + then move trackball to right.
  • +
  • CTRL + A = Press trackball once then tap the + "A" key on the soft keyboard.
  • +
  • F3 = Press trackball once then tap the + "3" key on the soft keyboard.
  • +
+ + + diff --git a/app/src/main/java/com/google/ase/Exec.java b/app/src/main/java/com/google/ase/Exec.java new file mode 100644 index 0000000..016fdf3 --- /dev/null +++ b/app/src/main/java/com/google/ase/Exec.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ase; + +import java.io.FileDescriptor; + +/** + * Tools for executing commands. + */ +public class Exec { + /** + * @param cmd + * The command to execute + * @param arg0 + * The first argument to the command, may be null + * @param arg1 + * the second argument to the command, may be null + * @return the file descriptor of the started process. + * + */ + public static FileDescriptor createSubprocess(String cmd, String arg0, String arg1) { + return createSubprocess(cmd, arg0, arg1, null); + } + + /** + * @param cmd + * The command to execute + * @param arg0 + * The first argument to the command, may be null + * @param arg1 + * the second argument to the command, may be null + * @param processId + * A one-element array to which the process ID of the started process will be written. + * @return the file descriptor of the started process. + * + */ + public static native FileDescriptor createSubprocess(String cmd, String arg0, String arg1, + int[] processId); + + public static native void setPtyWindowSize(FileDescriptor fd, int row, int col, int xpixel, + int ypixel); + + /** + * Causes the calling thread to wait for the process associated with the receiver to finish + * executing. + * + * @return The exit value of the Process being waited on + * + */ + public static native int waitFor(int processId); + + static { + System.loadLibrary("com_google_ase_Exec"); + } +} diff --git a/app/src/main/java/com/jcraft/jzlib/Adler32.java b/app/src/main/java/com/jcraft/jzlib/Adler32.java new file mode 100644 index 0000000..d8b6ef8 --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/Adler32.java @@ -0,0 +1,94 @@ +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +final class Adler32{ + + // largest prime smaller than 65536 + static final private int BASE=65521; + // NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 + static final private int NMAX=5552; + + long adler32(long adler, byte[] buf, int index, int len){ + if(buf == null){ return 1L; } + + long s1=adler&0xffff; + long s2=(adler>>16)&0xffff; + int k; + + while(len > 0) { + k=len=16){ + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + s1+=buf[index++]&0xff; s2+=s1; + k-=16; + } + if(k!=0){ + do{ + s1+=buf[index++]&0xff; s2+=s1; + } + while(--k!=0); + } + s1%=BASE; + s2%=BASE; + } + return (s2<<16)|s1; + } + + /* + private java.util.zip.Adler32 adler=new java.util.zip.Adler32(); + long adler32(long value, byte[] buf, int index, int len){ + if(value==1) {adler.reset();} + if(buf==null) {adler.reset();} + else{adler.update(buf, index, len);} + return adler.getValue(); + } + */ +} diff --git a/app/src/main/java/com/jcraft/jzlib/Deflate.java b/app/src/main/java/com/jcraft/jzlib/Deflate.java new file mode 100644 index 0000000..9978802 --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/Deflate.java @@ -0,0 +1,1623 @@ +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +public +final class Deflate{ + + static final private int MAX_MEM_LEVEL=9; + + static final private int Z_DEFAULT_COMPRESSION=-1; + + static final private int MAX_WBITS=15; // 32K LZ77 window + static final private int DEF_MEM_LEVEL=8; + + static class Config{ + int good_length; // reduce lazy search above this match length + int max_lazy; // do not perform lazy search above this match length + int nice_length; // quit search above this match length + int max_chain; + int func; + Config(int good_length, int max_lazy, + int nice_length, int max_chain, int func){ + this.good_length=good_length; + this.max_lazy=max_lazy; + this.nice_length=nice_length; + this.max_chain=max_chain; + this.func=func; + } + } + + static final private int STORED=0; + static final private int FAST=1; + static final private int SLOW=2; + static final private Config[] config_table; + static{ + config_table=new Config[10]; + // good lazy nice chain + config_table[0]=new Config(0, 0, 0, 0, STORED); + config_table[1]=new Config(4, 4, 8, 4, FAST); + config_table[2]=new Config(4, 5, 16, 8, FAST); + config_table[3]=new Config(4, 6, 32, 32, FAST); + + config_table[4]=new Config(4, 4, 16, 16, SLOW); + config_table[5]=new Config(8, 16, 32, 32, SLOW); + config_table[6]=new Config(8, 16, 128, 128, SLOW); + config_table[7]=new Config(8, 32, 128, 256, SLOW); + config_table[8]=new Config(32, 128, 258, 1024, SLOW); + config_table[9]=new Config(32, 258, 258, 4096, SLOW); + } + + static final private String[] z_errmsg = { + "need dictionary", // Z_NEED_DICT 2 + "stream end", // Z_STREAM_END 1 + "", // Z_OK 0 + "file error", // Z_ERRNO (-1) + "stream error", // Z_STREAM_ERROR (-2) + "data error", // Z_DATA_ERROR (-3) + "insufficient memory", // Z_MEM_ERROR (-4) + "buffer error", // Z_BUF_ERROR (-5) + "incompatible version",// Z_VERSION_ERROR (-6) + "" + }; + + // block not completed, need more input or more output + static final private int NeedMore=0; + + // block flush performed + static final private int BlockDone=1; + + // finish started, need only more output at next deflate + static final private int FinishStarted=2; + + // finish done, accept no more input or output + static final private int FinishDone=3; + + // preset dictionary flag in zlib header + static final private int PRESET_DICT=0x20; + + static final private int Z_FILTERED=1; + static final private int Z_HUFFMAN_ONLY=2; + static final private int Z_DEFAULT_STRATEGY=0; + + static final private int Z_NO_FLUSH=0; + static final private int Z_PARTIAL_FLUSH=1; + static final private int Z_SYNC_FLUSH=2; + static final private int Z_FULL_FLUSH=3; + static final private int Z_FINISH=4; + + static final private int Z_OK=0; + static final private int Z_STREAM_END=1; + static final private int Z_NEED_DICT=2; + static final private int Z_ERRNO=-1; + static final private int Z_STREAM_ERROR=-2; + static final private int Z_DATA_ERROR=-3; + static final private int Z_MEM_ERROR=-4; + static final private int Z_BUF_ERROR=-5; + static final private int Z_VERSION_ERROR=-6; + + static final private int INIT_STATE=42; + static final private int BUSY_STATE=113; + static final private int FINISH_STATE=666; + + // The deflate compression method + static final private int Z_DEFLATED=8; + + static final private int STORED_BLOCK=0; + static final private int STATIC_TREES=1; + static final private int DYN_TREES=2; + + // The three kinds of block type + static final private int Z_BINARY=0; + static final private int Z_ASCII=1; + static final private int Z_UNKNOWN=2; + + static final private int Buf_size=8*2; + + // repeat previous bit length 3-6 times (2 bits of repeat count) + static final private int REP_3_6=16; + + // repeat a zero length 3-10 times (3 bits of repeat count) + static final private int REPZ_3_10=17; + + // repeat a zero length 11-138 times (7 bits of repeat count) + static final private int REPZ_11_138=18; + + static final private int MIN_MATCH=3; + static final private int MAX_MATCH=258; + static final private int MIN_LOOKAHEAD=(MAX_MATCH+MIN_MATCH+1); + + static final private int MAX_BITS=15; + static final private int D_CODES=30; + static final private int BL_CODES=19; + static final private int LENGTH_CODES=29; + static final private int LITERALS=256; + static final private int L_CODES=(LITERALS+1+LENGTH_CODES); + static final private int HEAP_SIZE=(2*L_CODES+1); + + static final private int END_BLOCK=256; + + ZStream strm; // pointer back to this zlib stream + int status; // as the name implies + byte[] pending_buf; // output still pending + int pending_buf_size; // size of pending_buf + int pending_out; // next pending byte to output to the stream + int pending; // nb of bytes in the pending buffer + int noheader; // suppress zlib header and adler32 + byte data_type; // UNKNOWN, BINARY or ASCII + byte method; // STORED (for zip only) or DEFLATED + int last_flush; // value of flush param for previous deflate call + + int w_size; // LZ77 window size (32K by default) + int w_bits; // log2(w_size) (8..16) + int w_mask; // w_size - 1 + + byte[] window; + // Sliding window. Input bytes are read into the second half of the window, + // and move to the first half later to keep a dictionary of at least wSize + // bytes. With this organization, matches are limited to a distance of + // wSize-MAX_MATCH bytes, but this ensures that IO is always + // performed with a length multiple of the block size. Also, it limits + // the window size to 64K, which is quite useful on MSDOS. + // To do: use the user input buffer as sliding window. + + int window_size; + // Actual size of window: 2*wSize, except when the user input buffer + // is directly used as sliding window. + + short[] prev; + // Link to older string with same hash index. To limit the size of this + // array to 64K, this link is maintained only for the last 32K strings. + // An index in this array is thus a window index modulo 32K. + + short[] head; // Heads of the hash chains or NIL. + + int ins_h; // hash index of string to be inserted + int hash_size; // number of elements in hash table + int hash_bits; // log2(hash_size) + int hash_mask; // hash_size-1 + + // Number of bits by which ins_h must be shifted at each input + // step. It must be such that after MIN_MATCH steps, the oldest + // byte no longer takes part in the hash key, that is: + // hash_shift * MIN_MATCH >= hash_bits + int hash_shift; + + // Window position at the beginning of the current output block. Gets + // negative when the window is moved backwards. + + int block_start; + + int match_length; // length of best match + int prev_match; // previous match + int match_available; // set if previous match exists + int strstart; // start of string to insert + int match_start; // start of matching string + int lookahead; // number of valid bytes ahead in window + + // Length of the best match at previous step. Matches not greater than this + // are discarded. This is used in the lazy match evaluation. + int prev_length; + + // To speed up deflation, hash chains are never searched beyond this + // length. A higher limit improves compression ratio but degrades the speed. + int max_chain_length; + + // Attempt to find a better match only when the current match is strictly + // smaller than this value. This mechanism is used only for compression + // levels >= 4. + int max_lazy_match; + + // Insert new strings in the hash table only if the match length is not + // greater than this length. This saves time but degrades compression. + // max_insert_length is used only for compression levels <= 3. + + int level; // compression level (1..9) + int strategy; // favor or force Huffman coding + + // Use a faster search when the previous match is longer than this + int good_match; + + // Stop searching when current match exceeds this + int nice_match; + + short[] dyn_ltree; // literal and length tree + short[] dyn_dtree; // distance tree + short[] bl_tree; // Huffman tree for bit lengths + + Tree l_desc=new Tree(); // desc for literal tree + Tree d_desc=new Tree(); // desc for distance tree + Tree bl_desc=new Tree(); // desc for bit length tree + + // number of codes at each bit length for an optimal tree + short[] bl_count=new short[MAX_BITS+1]; + + // heap used to build the Huffman trees + int[] heap=new int[2*L_CODES+1]; + + int heap_len; // number of elements in the heap + int heap_max; // element of largest frequency + // The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used. + // The same heap array is used to build all trees. + + // Depth of each subtree used as tie breaker for trees of equal frequency + byte[] depth=new byte[2*L_CODES+1]; + + int l_buf; // index for literals or lengths */ + + // Size of match buffer for literals/lengths. There are 4 reasons for + // limiting lit_bufsize to 64K: + // - frequencies can be kept in 16 bit counters + // - if compression is not successful for the first block, all input + // data is still in the window so we can still emit a stored block even + // when input comes from standard input. (This can also be done for + // all blocks if lit_bufsize is not greater than 32K.) + // - if compression is not successful for a file smaller than 64K, we can + // even emit a stored file instead of a stored block (saving 5 bytes). + // This is applicable only for zip (not gzip or zlib). + // - creating new Huffman trees less frequently may not provide fast + // adaptation to changes in the input data statistics. (Take for + // example a binary file with poorly compressible code followed by + // a highly compressible string table.) Smaller buffer sizes give + // fast adaptation but have of course the overhead of transmitting + // trees more frequently. + // - I can't count above 4 + int lit_bufsize; + + int last_lit; // running index in l_buf + + // Buffer for distances. To simplify the code, d_buf and l_buf have + // the same number of elements. To use different lengths, an extra flag + // array would be necessary. + + int d_buf; // index of pendig_buf + + int opt_len; // bit length of current block with optimal trees + int static_len; // bit length of current block with static trees + int matches; // number of string matches in current block + int last_eob_len; // bit length of EOB code for last block + + // Output buffer. bits are inserted starting at the bottom (least + // significant bits). + short bi_buf; + + // Number of valid bits in bi_buf. All bits above the last valid bit + // are always zero. + int bi_valid; + + Deflate(){ + dyn_ltree=new short[HEAP_SIZE*2]; + dyn_dtree=new short[(2*D_CODES+1)*2]; // distance tree + bl_tree=new short[(2*BL_CODES+1)*2]; // Huffman tree for bit lengths + } + + void lm_init() { + window_size=2*w_size; + + head[hash_size-1]=0; + for(int i=0; i= 3; max_blindex--) { + if (bl_tree[Tree.bl_order[max_blindex]*2+1] != 0) break; + } + // Update opt_len to include the bit length tree and counts + opt_len += 3*(max_blindex+1) + 5+5+4; + + return max_blindex; + } + + + // Send the header for a block using dynamic Huffman trees: the counts, the + // lengths of the bit length codes, the literal tree and the distance tree. + // IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4. + void send_all_trees(int lcodes, int dcodes, int blcodes){ + int rank; // index in bl_order + + send_bits(lcodes-257, 5); // not +255 as stated in appnote.txt + send_bits(dcodes-1, 5); + send_bits(blcodes-4, 4); // not -3 as stated in appnote.txt + for (rank = 0; rank < blcodes; rank++) { + send_bits(bl_tree[Tree.bl_order[rank]*2+1], 3); + } + send_tree(dyn_ltree, lcodes-1); // literal tree + send_tree(dyn_dtree, dcodes-1); // distance tree + } + + // Send a literal or distance tree in compressed form, using the codes in + // bl_tree. + void send_tree (short[] tree,// the tree to be sent + int max_code // and its largest code of non zero frequency + ){ + int n; // iterates over all tree elements + int prevlen = -1; // last emitted length + int curlen; // length of current code + int nextlen = tree[0*2+1]; // length of next code + int count = 0; // repeat count of the current code + int max_count = 7; // max repeat count + int min_count = 4; // min repeat count + + if (nextlen == 0){ max_count = 138; min_count = 3; } + + for (n = 0; n <= max_code; n++) { + curlen = nextlen; nextlen = tree[(n+1)*2+1]; + if(++count < max_count && curlen == nextlen) { + continue; + } + else if(count < min_count) { + do { send_code(curlen, bl_tree); } while (--count != 0); + } + else if(curlen != 0){ + if(curlen != prevlen){ + send_code(curlen, bl_tree); count--; + } + send_code(REP_3_6, bl_tree); + send_bits(count-3, 2); + } + else if(count <= 10){ + send_code(REPZ_3_10, bl_tree); + send_bits(count-3, 3); + } + else{ + send_code(REPZ_11_138, bl_tree); + send_bits(count-11, 7); + } + count = 0; prevlen = curlen; + if(nextlen == 0){ + max_count = 138; min_count = 3; + } + else if(curlen == nextlen){ + max_count = 6; min_count = 3; + } + else{ + max_count = 7; min_count = 4; + } + } + } + + // Output a byte on the stream. + // IN assertion: there is enough room in pending_buf. + final void put_byte(byte[] p, int start, int len){ + System.arraycopy(p, start, pending_buf, pending, len); + pending+=len; + } + + final void put_byte(byte c){ + pending_buf[pending++]=c; + } + final void put_short(int w) { + put_byte((byte)(w/*&0xff*/)); + put_byte((byte)(w>>>8)); + } + final void putShortMSB(int b){ + put_byte((byte)(b>>8)); + put_byte((byte)(b/*&0xff*/)); + } + + final void send_code(int c, short[] tree){ + int c2=c*2; + send_bits((tree[c2]&0xffff), (tree[c2+1]&0xffff)); + } + + void send_bits(int value, int length){ + int len = length; + if (bi_valid > (int)Buf_size - len) { + int val = value; +// bi_buf |= (val << bi_valid); + bi_buf |= ((val << bi_valid)&0xffff); + put_short(bi_buf); + bi_buf = (short)(val >>> (Buf_size - bi_valid)); + bi_valid += len - Buf_size; + } else { +// bi_buf |= (value) << bi_valid; + bi_buf |= (((value) << bi_valid)&0xffff); + bi_valid += len; + } + } + + // Send one empty static block to give enough lookahead for inflate. + // This takes 10 bits, of which 7 may remain in the bit buffer. + // The current inflate code requires 9 bits of lookahead. If the + // last two codes for the previous block (real code plus EOB) were coded + // on 5 bits or less, inflate may have only 5+3 bits of lookahead to decode + // the last real code. In this case we send two empty static blocks instead + // of one. (There are no problems if the previous block is stored or fixed.) + // To simplify the code, we assume the worst case of last real code encoded + // on one bit only. + void _tr_align(){ + send_bits(STATIC_TREES<<1, 3); + send_code(END_BLOCK, StaticTree.static_ltree); + + bi_flush(); + + // Of the 10 bits for the empty block, we have already sent + // (10 - bi_valid) bits. The lookahead for the last real code (before + // the EOB of the previous block) was thus at least one plus the length + // of the EOB plus what we have just sent of the empty static block. + if (1 + last_eob_len + 10 - bi_valid < 9) { + send_bits(STATIC_TREES<<1, 3); + send_code(END_BLOCK, StaticTree.static_ltree); + bi_flush(); + } + last_eob_len = 7; + } + + + // Save the match info and tally the frequency counts. Return true if + // the current block must be flushed. + boolean _tr_tally (int dist, // distance of matched string + int lc // match length-MIN_MATCH or unmatched char (if dist==0) + ){ + + pending_buf[d_buf+last_lit*2] = (byte)(dist>>>8); + pending_buf[d_buf+last_lit*2+1] = (byte)dist; + + pending_buf[l_buf+last_lit] = (byte)lc; last_lit++; + + if (dist == 0) { + // lc is the unmatched char + dyn_ltree[lc*2]++; + } + else { + matches++; + // Here, lc is the match length - MIN_MATCH + dist--; // dist = match distance - 1 + dyn_ltree[(Tree._length_code[lc]+LITERALS+1)*2]++; + dyn_dtree[Tree.d_code(dist)*2]++; + } + + if ((last_lit & 0x1fff) == 0 && level > 2) { + // Compute an upper bound for the compressed length + int out_length = last_lit*8; + int in_length = strstart - block_start; + int dcode; + for (dcode = 0; dcode < D_CODES; dcode++) { + out_length += (int)dyn_dtree[dcode*2] * + (5L+Tree.extra_dbits[dcode]); + } + out_length >>>= 3; + if ((matches < (last_lit/2)) && out_length < in_length/2) return true; + } + + return (last_lit == lit_bufsize-1); + // We avoid equality with lit_bufsize because of wraparound at 64K + // on 16 bit machines and because stored blocks are restricted to + // 64K-1 bytes. + } + + // Send the block data compressed using the given Huffman trees + void compress_block(short[] ltree, short[] dtree){ + int dist; // distance of matched string + int lc; // match length or unmatched char (if dist == 0) + int lx = 0; // running index in l_buf + int code; // the code to send + int extra; // number of extra bits to send + + if (last_lit != 0){ + do{ + dist=((pending_buf[d_buf+lx*2]<<8)&0xff00)| + (pending_buf[d_buf+lx*2+1]&0xff); + lc=(pending_buf[l_buf+lx])&0xff; lx++; + + if(dist == 0){ + send_code(lc, ltree); // send a literal byte + } + else{ + // Here, lc is the match length - MIN_MATCH + code = Tree._length_code[lc]; + + send_code(code+LITERALS+1, ltree); // send the length code + extra = Tree.extra_lbits[code]; + if(extra != 0){ + lc -= Tree.base_length[code]; + send_bits(lc, extra); // send the extra length bits + } + dist--; // dist is now the match distance - 1 + code = Tree.d_code(dist); + + send_code(code, dtree); // send the distance code + extra = Tree.extra_dbits[code]; + if (extra != 0) { + dist -= Tree.base_dist[code]; + send_bits(dist, extra); // send the extra distance bits + } + } // literal or match pair ? + + // Check that the overlay between pending_buf and d_buf+l_buf is ok: + } + while (lx < last_lit); + } + + send_code(END_BLOCK, ltree); + last_eob_len = ltree[END_BLOCK*2+1]; + } + + // Set the data type to ASCII or BINARY, using a crude approximation: + // binary if more than 20% of the bytes are <= 6 or >= 128, ascii otherwise. + // IN assertion: the fields freq of dyn_ltree are set and the total of all + // frequencies does not exceed 64K (to fit in an int on 16 bit machines). + void set_data_type(){ + int n = 0; + int ascii_freq = 0; + int bin_freq = 0; + while(n<7){ bin_freq += dyn_ltree[n*2]; n++;} + while(n<128){ ascii_freq += dyn_ltree[n*2]; n++;} + while(n (ascii_freq >>> 2) ? Z_BINARY : Z_ASCII); + } + + // Flush the bit buffer, keeping at most 7 bits in it. + void bi_flush(){ + if (bi_valid == 16) { + put_short(bi_buf); + bi_buf=0; + bi_valid=0; + } + else if (bi_valid >= 8) { + put_byte((byte)bi_buf); + bi_buf>>>=8; + bi_valid-=8; + } + } + + // Flush the bit buffer and align the output on a byte boundary + void bi_windup(){ + if (bi_valid > 8) { + put_short(bi_buf); + } else if (bi_valid > 0) { + put_byte((byte)bi_buf); + } + bi_buf = 0; + bi_valid = 0; + } + + // Copy a stored block, storing first the length and its + // one's complement if requested. + void copy_block(int buf, // the input data + int len, // its length + boolean header // true if block header must be written + ){ + int index=0; + bi_windup(); // align on byte boundary + last_eob_len = 8; // enough lookahead for inflate + + if (header) { + put_short((short)len); + put_short((short)~len); + } + + // while(len--!=0) { + // put_byte(window[buf+index]); + // index++; + // } + put_byte(window, buf, len); + } + + void flush_block_only(boolean eof){ + _tr_flush_block(block_start>=0 ? block_start : -1, + strstart-block_start, + eof); + block_start=strstart; + strm.flush_pending(); + } + + // Copy without compression as much as possible from the input stream, return + // the current block state. + // This function does not insert new strings in the dictionary since + // uncompressible data is probably not useful. This function is used + // only for the level=0 compression option. + // NOTE: this function should be optimized to avoid extra copying from + // window to pending_buf. + int deflate_stored(int flush){ + // Stored blocks are limited to 0xffff bytes, pending_buf is limited + // to pending_buf_size, and each stored block has a 5 byte header: + + int max_block_size = 0xffff; + int max_start; + + if(max_block_size > pending_buf_size - 5) { + max_block_size = pending_buf_size - 5; + } + + // Copy as much as possible from input to output: + while(true){ + // Fill the window as much as possible: + if(lookahead<=1){ + fill_window(); + if(lookahead==0 && flush==Z_NO_FLUSH) return NeedMore; + if(lookahead==0) break; // flush the current block + } + + strstart+=lookahead; + lookahead=0; + + // Emit a stored block if pending_buf will be full: + max_start=block_start+max_block_size; + if(strstart==0|| strstart>=max_start) { + // strstart == 0 is possible when wraparound on 16-bit machine + lookahead = (int)(strstart-max_start); + strstart = (int)max_start; + + flush_block_only(false); + if(strm.avail_out==0) return NeedMore; + + } + + // Flush if we may have to slide, otherwise block_start may become + // negative and the data will be gone: + if(strstart-block_start >= w_size-MIN_LOOKAHEAD) { + flush_block_only(false); + if(strm.avail_out==0) return NeedMore; + } + } + + flush_block_only(flush == Z_FINISH); + if(strm.avail_out==0) + return (flush == Z_FINISH) ? FinishStarted : NeedMore; + + return flush == Z_FINISH ? FinishDone : BlockDone; + } + + // Send a stored block + void _tr_stored_block(int buf, // input block + int stored_len, // length of input block + boolean eof // true if this is the last block for a file + ){ + send_bits((STORED_BLOCK<<1)+(eof?1:0), 3); // send block type + copy_block(buf, stored_len, true); // with header + } + + // Determine the best encoding for the current block: dynamic trees, static + // trees or store, and output the encoded block to the zip file. + void _tr_flush_block(int buf, // input block, or NULL if too old + int stored_len, // length of input block + boolean eof // true if this is the last block for a file + ) { + int opt_lenb, static_lenb;// opt_len and static_len in bytes + int max_blindex = 0; // index of last bit length code of non zero freq + + // Build the Huffman trees unless a stored block is forced + if(level > 0) { + // Check if the file is ascii or binary + if(data_type == Z_UNKNOWN) set_data_type(); + + // Construct the literal and distance trees + l_desc.build_tree(this); + + d_desc.build_tree(this); + + // At this point, opt_len and static_len are the total bit lengths of + // the compressed block data, excluding the tree representations. + + // Build the bit length tree for the above two trees, and get the index + // in bl_order of the last bit length code to send. + max_blindex=build_bl_tree(); + + // Determine the best encoding. Compute first the block length in bytes + opt_lenb=(opt_len+3+7)>>>3; + static_lenb=(static_len+3+7)>>>3; + + if(static_lenb<=opt_lenb) opt_lenb=static_lenb; + } + else { + opt_lenb=static_lenb=stored_len+5; // force a stored block + } + + if(stored_len+4<=opt_lenb && buf != -1){ + // 4: two words for the lengths + // The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE. + // Otherwise we can't have processed more than WSIZE input bytes since + // the last block flush, because compression would have been + // successful. If LIT_BUFSIZE <= WSIZE, it is never too late to + // transform a block into a stored block. + _tr_stored_block(buf, stored_len, eof); + } + else if(static_lenb == opt_lenb){ + send_bits((STATIC_TREES<<1)+(eof?1:0), 3); + compress_block(StaticTree.static_ltree, StaticTree.static_dtree); + } + else{ + send_bits((DYN_TREES<<1)+(eof?1:0), 3); + send_all_trees(l_desc.max_code+1, d_desc.max_code+1, max_blindex+1); + compress_block(dyn_ltree, dyn_dtree); + } + + // The above check is made mod 2^32, for files larger than 512 MB + // and uLong implemented on 32 bits. + + init_block(); + + if(eof){ + bi_windup(); + } + } + + // Fill the window when the lookahead becomes insufficient. + // Updates strstart and lookahead. + // + // IN assertion: lookahead < MIN_LOOKAHEAD + // OUT assertions: strstart <= window_size-MIN_LOOKAHEAD + // At least one byte has been read, or avail_in == 0; reads are + // performed for at least two bytes (required for the zip translate_eol + // option -- not supported here). + void fill_window(){ + int n, m; + int p; + int more; // Amount of free space at the end of the window. + + do{ + more = (window_size-lookahead-strstart); + + // Deal with !@#$% 64K limit: + if(more==0 && strstart==0 && lookahead==0){ + more = w_size; + } + else if(more==-1) { + // Very unlikely, but possible on 16 bit machine if strstart == 0 + // and lookahead == 1 (input done one byte at time) + more--; + + // If the window is almost full and there is insufficient lookahead, + // move the upper half to the lower one to make room in the upper half. + } + else if(strstart >= w_size+ w_size-MIN_LOOKAHEAD) { + System.arraycopy(window, w_size, window, 0, w_size); + match_start-=w_size; + strstart-=w_size; // we now have strstart >= MAX_DIST + block_start-=w_size; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). We slide even when level == 0 + // to keep the hash table consistent if we switch back to level > 0 + // later. (Using level 0 permanently is not an optimal usage of + // zlib, so we don't care about this pathological case.) + + n = hash_size; + p=n; + do { + m = (head[--p]&0xffff); + head[p]=(m>=w_size ? (short)(m-w_size) : 0); + } + while (--n != 0); + + n = w_size; + p = n; + do { + m = (prev[--p]&0xffff); + prev[p] = (m >= w_size ? (short)(m-w_size) : 0); + // If n is not on any hash chain, prev[n] is garbage but + // its value will never be used. + } + while (--n!=0); + more += w_size; + } + + if (strm.avail_in == 0) return; + + // If there was no sliding: + // strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 && + // more == window_size - lookahead - strstart + // => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1) + // => more >= window_size - 2*WSIZE + 2 + // In the BIG_MEM or MMAP case (not yet supported), + // window_size == input_size + MIN_LOOKAHEAD && + // strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD. + // Otherwise, window_size == 2*WSIZE so more >= 2. + // If there was sliding, more >= WSIZE. So in all cases, more >= 2. + + n = strm.read_buf(window, strstart + lookahead, more); + lookahead += n; + + // Initialize the hash value now that we have some input: + if(lookahead >= MIN_MATCH) { + ins_h = window[strstart]&0xff; + ins_h=(((ins_h)<= MIN_MATCH){ + ins_h=(((ins_h)<=MIN_MATCH){ + // check_match(strstart, match_start, match_length); + + bflush=_tr_tally(strstart-match_start, match_length-MIN_MATCH); + + lookahead -= match_length; + + // Insert new strings in the hash table only if the match length + // is not too large. This saves time but degrades compression. + if(match_length <= max_lazy_match && + lookahead >= MIN_MATCH) { + match_length--; // string at strstart already in hash table + do{ + strstart++; + + ins_h=((ins_h<= MIN_MATCH) { + ins_h=(((ins_h)< 4096))) { + + // If prev_match is also MIN_MATCH, match_start is garbage + // but we will ignore the current match anyway. + match_length = MIN_MATCH-1; + } + } + + // If there was a match at the previous step and the current + // match is not better, output the previous match: + if(prev_length >= MIN_MATCH && match_length <= prev_length) { + int max_insert = strstart + lookahead - MIN_MATCH; + // Do not insert strings in hash table beyond this. + + // check_match(strstart-1, prev_match, prev_length); + + bflush=_tr_tally(strstart-1-prev_match, prev_length - MIN_MATCH); + + // Insert in hash table all strings up to the end of the match. + // strstart-1 and strstart are already inserted. If there is not + // enough lookahead, the last two strings are not inserted in + // the hash table. + lookahead -= prev_length-1; + prev_length -= 2; + do{ + if(++strstart <= max_insert) { + ins_h=(((ins_h)<(w_size-MIN_LOOKAHEAD) ? + strstart-(w_size-MIN_LOOKAHEAD) : 0; + int nice_match=this.nice_match; + + // Stop when cur_match becomes <= limit. To simplify the code, + // we prevent matches with the string of window index 0. + + int wmask = w_mask; + + int strend = strstart + MAX_MATCH; + byte scan_end1 = window[scan+best_len-1]; + byte scan_end = window[scan+best_len]; + + // The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. + // It is easy to get rid of this optimization if necessary. + + // Do not waste too much time if we already have a good match: + if (prev_length >= good_match) { + chain_length >>= 2; + } + + // Do not look for matches beyond the end of the input. This is necessary + // to make deflate deterministic. + if (nice_match > lookahead) nice_match = lookahead; + + do { + match = cur_match; + + // Skip to next match if the match length cannot increase + // or if the match length is less than 2: + if (window[match+best_len] != scan_end || + window[match+best_len-1] != scan_end1 || + window[match] != window[scan] || + window[++match] != window[scan+1]) continue; + + // The check at best_len-1 can be removed because it will be made + // again later. (This heuristic is not always a win.) + // It is not necessary to compare scan[2] and match[2] since they + // are always equal when the other bytes match, given that + // the hash keys are equal and that HASH_BITS >= 8. + scan += 2; match++; + + // We check for insufficient lookahead only every 8th comparison; + // the 256th check will be made at strstart+258. + do { + } while (window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + scan < strend); + + len = MAX_MATCH - (int)(strend - scan); + scan = strend - MAX_MATCH; + + if(len>best_len) { + match_start = cur_match; + best_len = len; + if (len >= nice_match) break; + scan_end1 = window[scan+best_len-1]; + scan_end = window[scan+best_len]; + } + + } while ((cur_match = (prev[cur_match & wmask]&0xffff)) > limit + && --chain_length != 0); + + if (best_len <= lookahead) return best_len; + return lookahead; + } + + int deflateInit(ZStream strm, int level, int bits){ + return deflateInit2(strm, level, Z_DEFLATED, bits, DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY); + } + int deflateInit(ZStream strm, int level){ + return deflateInit(strm, level, MAX_WBITS); + } + int deflateInit2(ZStream strm, int level, int method, int windowBits, + int memLevel, int strategy){ + int noheader = 0; + // byte[] my_version=ZLIB_VERSION; + + // + // if (version == null || version[0] != my_version[0] + // || stream_size != sizeof(z_stream)) { + // return Z_VERSION_ERROR; + // } + + strm.msg = null; + + if (level == Z_DEFAULT_COMPRESSION) level = 6; + + if (windowBits < 0) { // undocumented feature: suppress zlib header + noheader = 1; + windowBits = -windowBits; + } + + if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || + method != Z_DEFLATED || + windowBits < 9 || windowBits > 15 || level < 0 || level > 9 || + strategy < 0 || strategy > Z_HUFFMAN_ONLY) { + return Z_STREAM_ERROR; + } + + strm.dstate = (Deflate)this; + + this.noheader = noheader; + w_bits = windowBits; + w_size = 1 << w_bits; + w_mask = w_size - 1; + + hash_bits = memLevel + 7; + hash_size = 1 << hash_bits; + hash_mask = hash_size - 1; + hash_shift = ((hash_bits+MIN_MATCH-1)/MIN_MATCH); + + window = new byte[w_size*2]; + prev = new short[w_size]; + head = new short[hash_size]; + + lit_bufsize = 1 << (memLevel + 6); // 16K elements by default + + // We overlay pending_buf and d_buf+l_buf. This works since the average + // output size for (length,distance) codes is <= 24 bits. + pending_buf = new byte[lit_bufsize*4]; + pending_buf_size = lit_bufsize*4; + + d_buf = lit_bufsize/2; + l_buf = (1+2)*lit_bufsize; + + this.level = level; + +//System.out.println("level="+level); + + this.strategy = strategy; + this.method = (byte)method; + + return deflateReset(strm); + } + + int deflateReset(ZStream strm){ + strm.total_in = strm.total_out = 0; + strm.msg = null; // + strm.data_type = Z_UNKNOWN; + + pending = 0; + pending_out = 0; + + if(noheader < 0) { + noheader = 0; // was set to -1 by deflate(..., Z_FINISH); + } + status = (noheader!=0) ? BUSY_STATE : INIT_STATE; + strm.adler=strm._adler.adler32(0, null, 0, 0); + + last_flush = Z_NO_FLUSH; + + tr_init(); + lm_init(); + return Z_OK; + } + + int deflateEnd(){ + if(status!=INIT_STATE && status!=BUSY_STATE && status!=FINISH_STATE){ + return Z_STREAM_ERROR; + } + // Deallocate in reverse order of allocations: + pending_buf=null; + head=null; + prev=null; + window=null; + // free + // dstate=null; + return status == BUSY_STATE ? Z_DATA_ERROR : Z_OK; + } + + int deflateParams(ZStream strm, int _level, int _strategy){ + int err=Z_OK; + + if(_level == Z_DEFAULT_COMPRESSION){ + _level = 6; + } + if(_level < 0 || _level > 9 || + _strategy < 0 || _strategy > Z_HUFFMAN_ONLY) { + return Z_STREAM_ERROR; + } + + if(config_table[level].func!=config_table[_level].func && + strm.total_in != 0) { + // Flush the last buffer: + err = strm.deflate(Z_PARTIAL_FLUSH); + } + + if(level != _level) { + level = _level; + max_lazy_match = config_table[level].max_lazy; + good_match = config_table[level].good_length; + nice_match = config_table[level].nice_length; + max_chain_length = config_table[level].max_chain; + } + strategy = _strategy; + return err; + } + + int deflateSetDictionary (ZStream strm, byte[] dictionary, int dictLength){ + int length = dictLength; + int index=0; + + if(dictionary == null || status != INIT_STATE) + return Z_STREAM_ERROR; + + strm.adler=strm._adler.adler32(strm.adler, dictionary, 0, dictLength); + + if(length < MIN_MATCH) return Z_OK; + if(length > w_size-MIN_LOOKAHEAD){ + length = w_size-MIN_LOOKAHEAD; + index=dictLength-length; // use the tail of the dictionary + } + System.arraycopy(dictionary, index, window, 0, length); + strstart = length; + block_start = length; + + // Insert all strings in the hash table (except for the last two bytes). + // s->lookahead stays null, so s->ins_h will be recomputed at the next + // call of fill_window. + + ins_h = window[0]&0xff; + ins_h=(((ins_h)<Z_FINISH || flush<0){ + return Z_STREAM_ERROR; + } + + if(strm.next_out == null || + (strm.next_in == null && strm.avail_in != 0) || + (status == FINISH_STATE && flush != Z_FINISH)) { + strm.msg=z_errmsg[Z_NEED_DICT-(Z_STREAM_ERROR)]; + return Z_STREAM_ERROR; + } + if(strm.avail_out == 0){ + strm.msg=z_errmsg[Z_NEED_DICT-(Z_BUF_ERROR)]; + return Z_BUF_ERROR; + } + + this.strm = strm; // just in case + old_flush = last_flush; + last_flush = flush; + + // Write the zlib header + if(status == INIT_STATE) { + int header = (Z_DEFLATED+((w_bits-8)<<4))<<8; + int level_flags=((level-1)&0xff)>>1; + + if(level_flags>3) level_flags=3; + header |= (level_flags<<6); + if(strstart!=0) header |= PRESET_DICT; + header+=31-(header % 31); + + status=BUSY_STATE; + putShortMSB(header); + + + // Save the adler32 of the preset dictionary: + if(strstart!=0){ + putShortMSB((int)(strm.adler>>>16)); + putShortMSB((int)(strm.adler&0xffff)); + } + strm.adler=strm._adler.adler32(0, null, 0, 0); + } + + // Flush as much pending output as possible + if(pending != 0) { + strm.flush_pending(); + if(strm.avail_out == 0) { + //System.out.println(" avail_out==0"); + // Since avail_out is 0, deflate will be called again with + // more output space, but possibly with both pending and + // avail_in equal to zero. There won't be anything to do, + // but this is not an error situation so make sure we + // return OK instead of BUF_ERROR at next call of deflate: + last_flush = -1; + return Z_OK; + } + + // Make sure there is something to do and avoid duplicate consecutive + // flushes. For repeated and useless calls with Z_FINISH, we keep + // returning Z_STREAM_END instead of Z_BUFF_ERROR. + } + else if(strm.avail_in==0 && flush <= old_flush && + flush != Z_FINISH) { + strm.msg=z_errmsg[Z_NEED_DICT-(Z_BUF_ERROR)]; + return Z_BUF_ERROR; + } + + // User must not provide more input after the first FINISH: + if(status == FINISH_STATE && strm.avail_in != 0) { + strm.msg=z_errmsg[Z_NEED_DICT-(Z_BUF_ERROR)]; + return Z_BUF_ERROR; + } + + // Start a new block or continue the current one. + if(strm.avail_in!=0 || lookahead!=0 || + (flush != Z_NO_FLUSH && status != FINISH_STATE)) { + int bstate=-1; + switch(config_table[level].func){ + case STORED: + bstate = deflate_stored(flush); + break; + case FAST: + bstate = deflate_fast(flush); + break; + case SLOW: + bstate = deflate_slow(flush); + break; + default: + } + + if (bstate==FinishStarted || bstate==FinishDone) { + status = FINISH_STATE; + } + if (bstate==NeedMore || bstate==FinishStarted) { + if(strm.avail_out == 0) { + last_flush = -1; // avoid BUF_ERROR next call, see above + } + return Z_OK; + // If flush != Z_NO_FLUSH && avail_out == 0, the next call + // of deflate should use the same flush parameter to make sure + // that the flush is complete. So we don't have to output an + // empty block here, this will be done at next call. This also + // ensures that for a very small output buffer, we emit at most + // one empty block. + } + + if (bstate==BlockDone) { + if(flush == Z_PARTIAL_FLUSH) { + _tr_align(); + } + else { // FULL_FLUSH or SYNC_FLUSH + _tr_stored_block(0, 0, false); + // For a full flush, this empty block will be recognized + // as a special marker by inflate_sync(). + if(flush == Z_FULL_FLUSH) { + //state.head[s.hash_size-1]=0; + for(int i=0; i>>16)); + putShortMSB((int)(strm.adler&0xffff)); + strm.flush_pending(); + + // If avail_out is zero, the application will call deflate again + // to flush the rest. + noheader = -1; // write the trailer only once! + return pending != 0 ? Z_OK : Z_STREAM_END; + } +} diff --git a/app/src/main/java/com/jcraft/jzlib/InfBlocks.java b/app/src/main/java/com/jcraft/jzlib/InfBlocks.java new file mode 100644 index 0000000..f6997fc --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/InfBlocks.java @@ -0,0 +1,614 @@ +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +final class InfBlocks{ + static final private int MANY=1440; + + // And'ing with mask[n] masks the lower n bits + static final private int[] inflate_mask = { + 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, + 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, + 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, + 0x00007fff, 0x0000ffff + }; + + // Table for deflate from PKZIP's appnote.txt. + static final int[] border = { // Order of the bit length code lengths + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + }; + + static final private int Z_OK=0; + static final private int Z_STREAM_END=1; + static final private int Z_NEED_DICT=2; + static final private int Z_ERRNO=-1; + static final private int Z_STREAM_ERROR=-2; + static final private int Z_DATA_ERROR=-3; + static final private int Z_MEM_ERROR=-4; + static final private int Z_BUF_ERROR=-5; + static final private int Z_VERSION_ERROR=-6; + + static final private int TYPE=0; // get type bits (3, including end bit) + static final private int LENS=1; // get lengths for stored + static final private int STORED=2;// processing stored block + static final private int TABLE=3; // get table lengths + static final private int BTREE=4; // get bit lengths tree for a dynamic block + static final private int DTREE=5; // get length, distance trees for a dynamic block + static final private int CODES=6; // processing fixed or dynamic block + static final private int DRY=7; // output remaining window bytes + static final private int DONE=8; // finished last block, done + static final private int BAD=9; // ot a data error--stuck here + + int mode; // current inflate_block mode + + int left; // if STORED, bytes left to copy + + int table; // table lengths (14 bits) + int index; // index into blens (or border) + int[] blens; // bit lengths of codes + int[] bb=new int[1]; // bit length tree depth + int[] tb=new int[1]; // bit length decoding tree + + InfCodes codes=new InfCodes(); // if CODES, current state + + int last; // true if this block is the last block + + // mode independent information + int bitk; // bits in bit buffer + int bitb; // bit buffer + int[] hufts; // single malloc for tree space + byte[] window; // sliding window + int end; // one byte after sliding window + int read; // window read pointer + int write; // window write pointer + Object checkfn; // check function + long check; // check on output + + InfTree inftree=new InfTree(); + + InfBlocks(ZStream z, Object checkfn, int w){ + hufts=new int[MANY*3]; + window=new byte[w]; + end=w; + this.checkfn = checkfn; + mode = TYPE; + reset(z, null); + } + + void reset(ZStream z, long[] c){ + if(c!=null) c[0]=check; + if(mode==BTREE || mode==DTREE){ + } + if(mode==CODES){ + codes.free(z); + } + mode=TYPE; + bitk=0; + bitb=0; + read=write=0; + + if(checkfn != null) + z.adler=check=z._adler.adler32(0L, null, 0, 0); + } + + int proc(ZStream z, int r){ + int t; // temporary storage + int b; // bit buffer + int k; // bits in bit buffer + int p; // input data pointer + int n; // bytes available there + int q; // output window write pointer + int m; // bytes to end of window or read pointer + + // copy input/output information to locals (UPDATE macro restores) + {p=z.next_in_index;n=z.avail_in;b=bitb;k=bitk;} + {q=write;m=(int)(q>> 1){ + case 0: // stored + {b>>>=(3);k-=(3);} + t = k & 7; // go to byte boundary + + {b>>>=(t);k-=(t);} + mode = LENS; // get length of stored block + break; + case 1: // fixed + { + int[] bl=new int[1]; + int[] bd=new int[1]; + int[][] tl=new int[1][]; + int[][] td=new int[1][]; + + InfTree.inflate_trees_fixed(bl, bd, tl, td, z); + codes.init(bl[0], bd[0], tl[0], 0, td[0], 0, z); + } + + {b>>>=(3);k-=(3);} + + mode = CODES; + break; + case 2: // dynamic + + {b>>>=(3);k-=(3);} + + mode = TABLE; + break; + case 3: // illegal + + {b>>>=(3);k-=(3);} + mode = BAD; + z.msg = "invalid block type"; + r = Z_DATA_ERROR; + + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + break; + case LENS: + + while(k<(32)){ + if(n!=0){ + r=Z_OK; + } + else{ + bitb=b; bitk=k; + z.avail_in=n; + z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + }; + n--; + b|=(z.next_in[p++]&0xff)<>> 16) & 0xffff) != (b & 0xffff)){ + mode = BAD; + z.msg = "invalid stored block lengths"; + r = Z_DATA_ERROR; + + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + left = (b & 0xffff); + b = k = 0; // dump bits + mode = left!=0 ? STORED : (last!=0 ? DRY : TYPE); + break; + case STORED: + if (n == 0){ + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + + if(m==0){ + if(q==end&&read!=0){ + q=0; m=(int)(qn) t = n; + if(t>m) t = m; + System.arraycopy(z.next_in, p, window, q, t); + p += t; n -= t; + q += t; m -= t; + if ((left -= t) != 0) + break; + mode = last!=0 ? DRY : TYPE; + break; + case TABLE: + + while(k<(14)){ + if(n!=0){ + r=Z_OK; + } + else{ + bitb=b; bitk=k; + z.avail_in=n; + z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + }; + n--; + b|=(z.next_in[p++]&0xff)< 29 || ((t >> 5) & 0x1f) > 29) + { + mode = BAD; + z.msg = "too many length or distance symbols"; + r = Z_DATA_ERROR; + + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + t = 258 + (t & 0x1f) + ((t >> 5) & 0x1f); + if(blens==null || blens.length>>=(14);k-=(14);} + + index = 0; + mode = BTREE; + case BTREE: + while (index < 4 + (table >>> 10)){ + while(k<(3)){ + if(n!=0){ + r=Z_OK; + } + else{ + bitb=b; bitk=k; + z.avail_in=n; + z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + }; + n--; + b|=(z.next_in[p++]&0xff)<>>=(3);k-=(3);} + } + + while(index < 19){ + blens[border[index++]] = 0; + } + + bb[0] = 7; + t = inftree.inflate_trees_bits(blens, bb, tb, hufts, z); + if (t != Z_OK){ + r = t; + if (r == Z_DATA_ERROR){ + blens=null; + mode = BAD; + } + + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + + index = 0; + mode = DTREE; + case DTREE: + while (true){ + t = table; + if(!(index < 258 + (t & 0x1f) + ((t >> 5) & 0x1f))){ + break; + } + + int[] h; + int i, j, c; + + t = bb[0]; + + while(k<(t)){ + if(n!=0){ + r=Z_OK; + } + else{ + bitb=b; bitk=k; + z.avail_in=n; + z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + }; + n--; + b|=(z.next_in[p++]&0xff)<>>=(t);k-=(t); + blens[index++] = c; + } + else { // c == 16..18 + i = c == 18 ? 7 : c - 14; + j = c == 18 ? 11 : 3; + + while(k<(t+i)){ + if(n!=0){ + r=Z_OK; + } + else{ + bitb=b; bitk=k; + z.avail_in=n; + z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + }; + n--; + b|=(z.next_in[p++]&0xff)<>>=(t);k-=(t); + + j += (b & inflate_mask[i]); + + b>>>=(i);k-=(i); + + i = index; + t = table; + if (i + j > 258 + (t & 0x1f) + ((t >> 5) & 0x1f) || + (c == 16 && i < 1)){ + blens=null; + mode = BAD; + z.msg = "invalid bit length repeat"; + r = Z_DATA_ERROR; + + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + + c = c == 16 ? blens[i-1] : 0; + do{ + blens[i++] = c; + } + while (--j!=0); + index = i; + } + } + + tb[0]=-1; + { + int[] bl=new int[1]; + int[] bd=new int[1]; + int[] tl=new int[1]; + int[] td=new int[1]; + bl[0] = 9; // must be <= 9 for lookahead assumptions + bd[0] = 6; // must be <= 9 for lookahead assumptions + + t = table; + t = inftree.inflate_trees_dynamic(257 + (t & 0x1f), + 1 + ((t >> 5) & 0x1f), + blens, bl, bd, tl, td, hufts, z); + + if (t != Z_OK){ + if (t == Z_DATA_ERROR){ + blens=null; + mode = BAD; + } + r = t; + + bitb=b; bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + return inflate_flush(z,r); + } + codes.init(bl[0], bd[0], hufts, tl[0], hufts, td[0], z); + } + mode = CODES; + case CODES: + bitb=b; bitk=k; + z.avail_in=n; z.total_in+=p-z.next_in_index;z.next_in_index=p; + write=q; + + if ((r = codes.proc(this, z, r)) != Z_STREAM_END){ + return inflate_flush(z, r); + } + r = Z_OK; + codes.free(z); + + p=z.next_in_index; n=z.avail_in;b=bitb;k=bitk; + q=write;m=(int)(q z.avail_out) n = z.avail_out; + if (n!=0 && r == Z_BUF_ERROR) r = Z_OK; + + // update counters + z.avail_out -= n; + z.total_out += n; + + // update check information + if(checkfn != null) + z.adler=check=z._adler.adler32(check, window, q, n); + + // copy as far as end of window + System.arraycopy(window, q, z.next_out, p, n); + p += n; + q += n; + + // see if more to copy at beginning of window + if (q == end){ + // wrap pointers + q = 0; + if (write == end) + write = 0; + + // compute bytes to copy + n = write - q; + if (n > z.avail_out) n = z.avail_out; + if (n!=0 && r == Z_BUF_ERROR) r = Z_OK; + + // update counters + z.avail_out -= n; + z.total_out += n; + + // update check information + if(checkfn != null) + z.adler=check=z._adler.adler32(check, window, q, n); + + // copy + System.arraycopy(window, q, z.next_out, p, n); + p += n; + q += n; + } + + // update pointers + z.next_out_index = p; + read = q; + + // done + return r; + } +} diff --git a/app/src/main/java/com/jcraft/jzlib/InfCodes.java b/app/src/main/java/com/jcraft/jzlib/InfCodes.java new file mode 100644 index 0000000..c768fb1 --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/InfCodes.java @@ -0,0 +1,605 @@ +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +final class InfCodes{ + + static final private int[] inflate_mask = { + 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, + 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, + 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, + 0x00007fff, 0x0000ffff + }; + + static final private int Z_OK=0; + static final private int Z_STREAM_END=1; + static final private int Z_NEED_DICT=2; + static final private int Z_ERRNO=-1; + static final private int Z_STREAM_ERROR=-2; + static final private int Z_DATA_ERROR=-3; + static final private int Z_MEM_ERROR=-4; + static final private int Z_BUF_ERROR=-5; + static final private int Z_VERSION_ERROR=-6; + + // waiting for "i:"=input, + // "o:"=output, + // "x:"=nothing + static final private int START=0; // x: set up for LEN + static final private int LEN=1; // i: get length/literal/eob next + static final private int LENEXT=2; // i: getting length extra (have base) + static final private int DIST=3; // i: get distance next + static final private int DISTEXT=4;// i: getting distance extra + static final private int COPY=5; // o: copying bytes in window, waiting for space + static final private int LIT=6; // o: got literal, waiting for output space + static final private int WASH=7; // o: got eob, possibly still output waiting + static final private int END=8; // x: got eob and all data flushed + static final private int BADCODE=9;// x: got error + + int mode; // current inflate_codes mode + + // mode dependent information + int len; + + int[] tree; // pointer into tree + int tree_index=0; + int need; // bits needed + + int lit; + + // if EXT or COPY, where and how much + int get; // bits to get for extra + int dist; // distance back to copy from + + byte lbits; // ltree bits decoded per branch + byte dbits; // dtree bits decoder per branch + int[] ltree; // literal/length/eob tree + int ltree_index; // literal/length/eob tree + int[] dtree; // distance tree + int dtree_index; // distance tree + + InfCodes(){ + } + void init(int bl, int bd, + int[] tl, int tl_index, + int[] td, int td_index, ZStream z){ + mode=START; + lbits=(byte)bl; + dbits=(byte)bd; + ltree=tl; + ltree_index=tl_index; + dtree = td; + dtree_index=td_index; + tree=null; + } + + int proc(InfBlocks s, ZStream z, int r){ + int j; // temporary storage + int[] t; // temporary pointer + int tindex; // temporary pointer + int e; // extra bits or operation + int b=0; // bit buffer + int k=0; // bits in bit buffer + int p=0; // input data pointer + int n; // bytes available there + int q; // output window write pointer + int m; // bytes to end of window or read pointer + int f; // pointer to copy strings from + + // copy input/output information to locals (UPDATE macro restores) + p=z.next_in_index;n=z.avail_in;b=s.bitb;k=s.bitk; + q=s.write;m=q= 258 && n >= 10){ + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + r = inflate_fast(lbits, dbits, + ltree, ltree_index, + dtree, dtree_index, + s, z); + + p=z.next_in_index;n=z.avail_in;b=s.bitb;k=s.bitk; + q=s.write;m=q>>=(tree[tindex+1]); + k-=(tree[tindex+1]); + + e=tree[tindex]; + + if(e == 0){ // literal + lit = tree[tindex+2]; + mode = LIT; + break; + } + if((e & 16)!=0 ){ // length + get = e & 15; + len = tree[tindex+2]; + mode = LENEXT; + break; + } + if ((e & 64) == 0){ // next table + need = e; + tree_index = tindex/3+tree[tindex+2]; + break; + } + if ((e & 32)!=0){ // end of block + mode = WASH; + break; + } + mode = BADCODE; // invalid code + z.msg = "invalid literal/length code"; + r = Z_DATA_ERROR; + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + return s.inflate_flush(z,r); + + case LENEXT: // i: getting length extra (have base) + j = get; + + while(k<(j)){ + if(n!=0)r=Z_OK; + else{ + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + return s.inflate_flush(z,r); + } + n--; b|=(z.next_in[p++]&0xff)<>=j; + k-=j; + + need = dbits; + tree = dtree; + tree_index=dtree_index; + mode = DIST; + case DIST: // i: get distance next + j = need; + + while(k<(j)){ + if(n!=0)r=Z_OK; + else{ + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + return s.inflate_flush(z,r); + } + n--; b|=(z.next_in[p++]&0xff)<>=tree[tindex+1]; + k-=tree[tindex+1]; + + e = (tree[tindex]); + if((e & 16)!=0){ // distance + get = e & 15; + dist = tree[tindex+2]; + mode = DISTEXT; + break; + } + if ((e & 64) == 0){ // next table + need = e; + tree_index = tindex/3 + tree[tindex+2]; + break; + } + mode = BADCODE; // invalid code + z.msg = "invalid distance code"; + r = Z_DATA_ERROR; + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + return s.inflate_flush(z,r); + + case DISTEXT: // i: getting distance extra + j = get; + + while(k<(j)){ + if(n!=0)r=Z_OK; + else{ + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + return s.inflate_flush(z,r); + } + n--; b|=(z.next_in[p++]&0xff)<>=j; + k-=j; + + mode = COPY; + case COPY: // o: copying bytes in window, waiting for space + f = q - dist; + while(f < 0){ // modulo window size-"while" instead + f += s.end; // of "if" handles invalid distances + } + while (len!=0){ + + if(m==0){ + if(q==s.end&&s.read!=0){q=0;m=q 7){ // return unused byte, if any + k -= 8; + n++; + p--; // can always return one + } + + s.write=q; r=s.inflate_flush(z,r); + q=s.write;m=q= 258 && n >= 10 + // get literal/length code + while(k<(20)){ // max bits for literal/length code + n--; + b|=(z.next_in[p++]&0xff)<>=(tp[tp_index_t_3+1]); k-=(tp[tp_index_t_3+1]); + + s.window[q++] = (byte)tp[tp_index_t_3+2]; + m--; + continue; + } + do { + + b>>=(tp[tp_index_t_3+1]); k-=(tp[tp_index_t_3+1]); + + if((e&16)!=0){ + e &= 15; + c = tp[tp_index_t_3+2] + ((int)b & inflate_mask[e]); + + b>>=e; k-=e; + + // decode distance base of block to copy + while(k<(15)){ // max bits for distance code + n--; + b|=(z.next_in[p++]&0xff)<>=(tp[tp_index_t_3+1]); k-=(tp[tp_index_t_3+1]); + + if((e&16)!=0){ + // get extra bits to add to distance base + e &= 15; + while(k<(e)){ // get extra bits (up to 13) + n--; + b|=(z.next_in[p++]&0xff)<>=(e); k-=(e); + + // do the copy + m -= c; + if (q >= d){ // offset before dest + // just copy + r=q-d; + if(q-r>0 && 2>(q-r)){ + s.window[q++]=s.window[r++]; // minimum count is three, + s.window[q++]=s.window[r++]; // so unroll loop a little + c-=2; + } + else{ + System.arraycopy(s.window, r, s.window, q, 2); + q+=2; r+=2; c-=2; + } + } + else{ // else offset after destination + r=q-d; + do{ + r+=s.end; // force pointer in window + }while(r<0); // covers invalid distances + e=s.end-r; + if(c>e){ // if source crosses, + c-=e; // wrapped copy + if(q-r>0 && e>(q-r)){ + do{s.window[q++] = s.window[r++];} + while(--e!=0); + } + else{ + System.arraycopy(s.window, r, s.window, q, e); + q+=e; r+=e; e=0; + } + r = 0; // copy rest from start of window + } + + } + + // copy all or what's left + if(q-r>0 && c>(q-r)){ + do{s.window[q++] = s.window[r++];} + while(--c!=0); + } + else{ + System.arraycopy(s.window, r, s.window, q, c); + q+=c; r+=c; c=0; + } + break; + } + else if((e&64)==0){ + t+=tp[tp_index_t_3+2]; + t+=(b&inflate_mask[e]); + tp_index_t_3=(tp_index+t)*3; + e=tp[tp_index_t_3]; + } + else{ + z.msg = "invalid distance code"; + + c=z.avail_in-n;c=(k>>3)>3:c;n+=c;p-=c;k-=c<<3; + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + + return Z_DATA_ERROR; + } + } + while(true); + break; + } + + if((e&64)==0){ + t+=tp[tp_index_t_3+2]; + t+=(b&inflate_mask[e]); + tp_index_t_3=(tp_index+t)*3; + if((e=tp[tp_index_t_3])==0){ + + b>>=(tp[tp_index_t_3+1]); k-=(tp[tp_index_t_3+1]); + + s.window[q++]=(byte)tp[tp_index_t_3+2]; + m--; + break; + } + } + else if((e&32)!=0){ + + c=z.avail_in-n;c=(k>>3)>3:c;n+=c;p-=c;k-=c<<3; + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + + return Z_STREAM_END; + } + else{ + z.msg="invalid literal/length code"; + + c=z.avail_in-n;c=(k>>3)>3:c;n+=c;p-=c;k-=c<<3; + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + + return Z_DATA_ERROR; + } + } + while(true); + } + while(m>=258 && n>= 10); + + // not enough input or output--restore pointers and return + c=z.avail_in-n;c=(k>>3)>3:c;n+=c;p-=c;k-=c<<3; + + s.bitb=b;s.bitk=k; + z.avail_in=n;z.total_in+=p-z.next_in_index;z.next_in_index=p; + s.write=q; + + return Z_OK; + } +} diff --git a/app/src/main/java/com/jcraft/jzlib/InfTree.java b/app/src/main/java/com/jcraft/jzlib/InfTree.java new file mode 100644 index 0000000..cbca436 --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/InfTree.java @@ -0,0 +1,520 @@ +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +final class InfTree{ + + static final private int MANY=1440; + + static final private int Z_OK=0; + static final private int Z_STREAM_END=1; + static final private int Z_NEED_DICT=2; + static final private int Z_ERRNO=-1; + static final private int Z_STREAM_ERROR=-2; + static final private int Z_DATA_ERROR=-3; + static final private int Z_MEM_ERROR=-4; + static final private int Z_BUF_ERROR=-5; + static final private int Z_VERSION_ERROR=-6; + + static final int fixed_bl = 9; + static final int fixed_bd = 5; + + static final int[] fixed_tl = { + 96,7,256, 0,8,80, 0,8,16, 84,8,115, + 82,7,31, 0,8,112, 0,8,48, 0,9,192, + 80,7,10, 0,8,96, 0,8,32, 0,9,160, + 0,8,0, 0,8,128, 0,8,64, 0,9,224, + 80,7,6, 0,8,88, 0,8,24, 0,9,144, + 83,7,59, 0,8,120, 0,8,56, 0,9,208, + 81,7,17, 0,8,104, 0,8,40, 0,9,176, + 0,8,8, 0,8,136, 0,8,72, 0,9,240, + 80,7,4, 0,8,84, 0,8,20, 85,8,227, + 83,7,43, 0,8,116, 0,8,52, 0,9,200, + 81,7,13, 0,8,100, 0,8,36, 0,9,168, + 0,8,4, 0,8,132, 0,8,68, 0,9,232, + 80,7,8, 0,8,92, 0,8,28, 0,9,152, + 84,7,83, 0,8,124, 0,8,60, 0,9,216, + 82,7,23, 0,8,108, 0,8,44, 0,9,184, + 0,8,12, 0,8,140, 0,8,76, 0,9,248, + 80,7,3, 0,8,82, 0,8,18, 85,8,163, + 83,7,35, 0,8,114, 0,8,50, 0,9,196, + 81,7,11, 0,8,98, 0,8,34, 0,9,164, + 0,8,2, 0,8,130, 0,8,66, 0,9,228, + 80,7,7, 0,8,90, 0,8,26, 0,9,148, + 84,7,67, 0,8,122, 0,8,58, 0,9,212, + 82,7,19, 0,8,106, 0,8,42, 0,9,180, + 0,8,10, 0,8,138, 0,8,74, 0,9,244, + 80,7,5, 0,8,86, 0,8,22, 192,8,0, + 83,7,51, 0,8,118, 0,8,54, 0,9,204, + 81,7,15, 0,8,102, 0,8,38, 0,9,172, + 0,8,6, 0,8,134, 0,8,70, 0,9,236, + 80,7,9, 0,8,94, 0,8,30, 0,9,156, + 84,7,99, 0,8,126, 0,8,62, 0,9,220, + 82,7,27, 0,8,110, 0,8,46, 0,9,188, + 0,8,14, 0,8,142, 0,8,78, 0,9,252, + 96,7,256, 0,8,81, 0,8,17, 85,8,131, + 82,7,31, 0,8,113, 0,8,49, 0,9,194, + 80,7,10, 0,8,97, 0,8,33, 0,9,162, + 0,8,1, 0,8,129, 0,8,65, 0,9,226, + 80,7,6, 0,8,89, 0,8,25, 0,9,146, + 83,7,59, 0,8,121, 0,8,57, 0,9,210, + 81,7,17, 0,8,105, 0,8,41, 0,9,178, + 0,8,9, 0,8,137, 0,8,73, 0,9,242, + 80,7,4, 0,8,85, 0,8,21, 80,8,258, + 83,7,43, 0,8,117, 0,8,53, 0,9,202, + 81,7,13, 0,8,101, 0,8,37, 0,9,170, + 0,8,5, 0,8,133, 0,8,69, 0,9,234, + 80,7,8, 0,8,93, 0,8,29, 0,9,154, + 84,7,83, 0,8,125, 0,8,61, 0,9,218, + 82,7,23, 0,8,109, 0,8,45, 0,9,186, + 0,8,13, 0,8,141, 0,8,77, 0,9,250, + 80,7,3, 0,8,83, 0,8,19, 85,8,195, + 83,7,35, 0,8,115, 0,8,51, 0,9,198, + 81,7,11, 0,8,99, 0,8,35, 0,9,166, + 0,8,3, 0,8,131, 0,8,67, 0,9,230, + 80,7,7, 0,8,91, 0,8,27, 0,9,150, + 84,7,67, 0,8,123, 0,8,59, 0,9,214, + 82,7,19, 0,8,107, 0,8,43, 0,9,182, + 0,8,11, 0,8,139, 0,8,75, 0,9,246, + 80,7,5, 0,8,87, 0,8,23, 192,8,0, + 83,7,51, 0,8,119, 0,8,55, 0,9,206, + 81,7,15, 0,8,103, 0,8,39, 0,9,174, + 0,8,7, 0,8,135, 0,8,71, 0,9,238, + 80,7,9, 0,8,95, 0,8,31, 0,9,158, + 84,7,99, 0,8,127, 0,8,63, 0,9,222, + 82,7,27, 0,8,111, 0,8,47, 0,9,190, + 0,8,15, 0,8,143, 0,8,79, 0,9,254, + 96,7,256, 0,8,80, 0,8,16, 84,8,115, + 82,7,31, 0,8,112, 0,8,48, 0,9,193, + + 80,7,10, 0,8,96, 0,8,32, 0,9,161, + 0,8,0, 0,8,128, 0,8,64, 0,9,225, + 80,7,6, 0,8,88, 0,8,24, 0,9,145, + 83,7,59, 0,8,120, 0,8,56, 0,9,209, + 81,7,17, 0,8,104, 0,8,40, 0,9,177, + 0,8,8, 0,8,136, 0,8,72, 0,9,241, + 80,7,4, 0,8,84, 0,8,20, 85,8,227, + 83,7,43, 0,8,116, 0,8,52, 0,9,201, + 81,7,13, 0,8,100, 0,8,36, 0,9,169, + 0,8,4, 0,8,132, 0,8,68, 0,9,233, + 80,7,8, 0,8,92, 0,8,28, 0,9,153, + 84,7,83, 0,8,124, 0,8,60, 0,9,217, + 82,7,23, 0,8,108, 0,8,44, 0,9,185, + 0,8,12, 0,8,140, 0,8,76, 0,9,249, + 80,7,3, 0,8,82, 0,8,18, 85,8,163, + 83,7,35, 0,8,114, 0,8,50, 0,9,197, + 81,7,11, 0,8,98, 0,8,34, 0,9,165, + 0,8,2, 0,8,130, 0,8,66, 0,9,229, + 80,7,7, 0,8,90, 0,8,26, 0,9,149, + 84,7,67, 0,8,122, 0,8,58, 0,9,213, + 82,7,19, 0,8,106, 0,8,42, 0,9,181, + 0,8,10, 0,8,138, 0,8,74, 0,9,245, + 80,7,5, 0,8,86, 0,8,22, 192,8,0, + 83,7,51, 0,8,118, 0,8,54, 0,9,205, + 81,7,15, 0,8,102, 0,8,38, 0,9,173, + 0,8,6, 0,8,134, 0,8,70, 0,9,237, + 80,7,9, 0,8,94, 0,8,30, 0,9,157, + 84,7,99, 0,8,126, 0,8,62, 0,9,221, + 82,7,27, 0,8,110, 0,8,46, 0,9,189, + 0,8,14, 0,8,142, 0,8,78, 0,9,253, + 96,7,256, 0,8,81, 0,8,17, 85,8,131, + 82,7,31, 0,8,113, 0,8,49, 0,9,195, + 80,7,10, 0,8,97, 0,8,33, 0,9,163, + 0,8,1, 0,8,129, 0,8,65, 0,9,227, + 80,7,6, 0,8,89, 0,8,25, 0,9,147, + 83,7,59, 0,8,121, 0,8,57, 0,9,211, + 81,7,17, 0,8,105, 0,8,41, 0,9,179, + 0,8,9, 0,8,137, 0,8,73, 0,9,243, + 80,7,4, 0,8,85, 0,8,21, 80,8,258, + 83,7,43, 0,8,117, 0,8,53, 0,9,203, + 81,7,13, 0,8,101, 0,8,37, 0,9,171, + 0,8,5, 0,8,133, 0,8,69, 0,9,235, + 80,7,8, 0,8,93, 0,8,29, 0,9,155, + 84,7,83, 0,8,125, 0,8,61, 0,9,219, + 82,7,23, 0,8,109, 0,8,45, 0,9,187, + 0,8,13, 0,8,141, 0,8,77, 0,9,251, + 80,7,3, 0,8,83, 0,8,19, 85,8,195, + 83,7,35, 0,8,115, 0,8,51, 0,9,199, + 81,7,11, 0,8,99, 0,8,35, 0,9,167, + 0,8,3, 0,8,131, 0,8,67, 0,9,231, + 80,7,7, 0,8,91, 0,8,27, 0,9,151, + 84,7,67, 0,8,123, 0,8,59, 0,9,215, + 82,7,19, 0,8,107, 0,8,43, 0,9,183, + 0,8,11, 0,8,139, 0,8,75, 0,9,247, + 80,7,5, 0,8,87, 0,8,23, 192,8,0, + 83,7,51, 0,8,119, 0,8,55, 0,9,207, + 81,7,15, 0,8,103, 0,8,39, 0,9,175, + 0,8,7, 0,8,135, 0,8,71, 0,9,239, + 80,7,9, 0,8,95, 0,8,31, 0,9,159, + 84,7,99, 0,8,127, 0,8,63, 0,9,223, + 82,7,27, 0,8,111, 0,8,47, 0,9,191, + 0,8,15, 0,8,143, 0,8,79, 0,9,255 + }; + static final int[] fixed_td = { + 80,5,1, 87,5,257, 83,5,17, 91,5,4097, + 81,5,5, 89,5,1025, 85,5,65, 93,5,16385, + 80,5,3, 88,5,513, 84,5,33, 92,5,8193, + 82,5,9, 90,5,2049, 86,5,129, 192,5,24577, + 80,5,2, 87,5,385, 83,5,25, 91,5,6145, + 81,5,7, 89,5,1537, 85,5,97, 93,5,24577, + 80,5,4, 88,5,769, 84,5,49, 92,5,12289, + 82,5,13, 90,5,3073, 86,5,193, 192,5,24577 + }; + + // Tables for deflate from PKZIP's appnote.txt. + static final int[] cplens = { // Copy lengths for literal codes 257..285 + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 + }; + + // see note #13 above about 258 + static final int[] cplext = { // Extra bits for literal codes 257..285 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 112, 112 // 112==invalid + }; + + static final int[] cpdist = { // Copy offsets for distance codes 0..29 + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 + }; + + static final int[] cpdext = { // Extra bits for distance codes + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13}; + + // If BMAX needs to be larger than 16, then h and x[] should be uLong. + static final int BMAX=15; // maximum bit length of any code + + int[] hn = null; // hufts used in space + int[] v = null; // work area for huft_build + int[] c = null; // bit length count table + int[] r = null; // table entry for structure assignment + int[] u = null; // table stack + int[] x = null; // bit offsets, then code stack + + private int huft_build(int[] b, // code lengths in bits (all assumed <= BMAX) + int bindex, + int n, // number of codes (assumed <= 288) + int s, // number of simple-valued codes (0..s-1) + int[] d, // list of base values for non-simple codes + int[] e, // list of extra bits for non-simple codes + int[] t, // result: starting table + int[] m, // maximum lookup bits, returns actual + int[] hp,// space for trees + int[] hn,// hufts used in space + int[] v // working area: values in order of bit length + ){ + // Given a list of code lengths and a maximum table size, make a set of + // tables to decode that set of codes. Return Z_OK on success, Z_BUF_ERROR + // if the given code set is incomplete (the tables are still built in this + // case), Z_DATA_ERROR if the input is invalid (an over-subscribed set of + // lengths), or Z_MEM_ERROR if not enough memory. + + int a; // counter for codes of length k + int f; // i repeats in table every f entries + int g; // maximum code length + int h; // table level + int i; // counter, current code + int j; // counter + int k; // number of bits in current code + int l; // bits per table (returned in m) + int mask; // (1 << w) - 1, to avoid cc -O bug on HP + int p; // pointer into c[], b[], or v[] + int q; // points to current table + int w; // bits before this table == (l * h) + int xp; // pointer into x + int y; // number of dummy codes added + int z; // number of entries in current table + + // Generate counts for each bit length + + p = 0; i = n; + do { + c[b[bindex+p]]++; p++; i--; // assume all entries <= BMAX + }while(i!=0); + + if(c[0] == n){ // null input--all zero length codes + t[0] = -1; + m[0] = 0; + return Z_OK; + } + + // Find minimum and maximum length, bound *m by those + l = m[0]; + for (j = 1; j <= BMAX; j++) + if(c[j]!=0) break; + k = j; // minimum code length + if(l < j){ + l = j; + } + for (i = BMAX; i!=0; i--){ + if(c[i]!=0) break; + } + g = i; // maximum code length + if(l > i){ + l = i; + } + m[0] = l; + + // Adjust last length count to fill out codes, if needed + for (y = 1 << j; j < i; j++, y <<= 1){ + if ((y -= c[j]) < 0){ + return Z_DATA_ERROR; + } + } + if ((y -= c[i]) < 0){ + return Z_DATA_ERROR; + } + c[i] += y; + + // Generate starting offsets into the value table for each length + x[1] = j = 0; + p = 1; xp = 2; + while (--i!=0) { // note that i == g from above + x[xp] = (j += c[p]); + xp++; + p++; + } + + // Make a table of values in order of bit lengths + i = 0; p = 0; + do { + if ((j = b[bindex+p]) != 0){ + v[x[j]++] = i; + } + p++; + } + while (++i < n); + n = x[g]; // set n to length of v + + // Generate the Huffman codes and for each, make the table entries + x[0] = i = 0; // first Huffman code is zero + p = 0; // grab values in bit order + h = -1; // no tables yet--level -1 + w = -l; // bits decoded == (l * h) + u[0] = 0; // just to keep compilers happy + q = 0; // ditto + z = 0; // ditto + + // go through the bit lengths (k already is bits in shortest code) + for (; k <= g; k++){ + a = c[k]; + while (a--!=0){ + // here i is the Huffman code of length k bits for value *p + // make tables up to required level + while (k > w + l){ + h++; + w += l; // previous table always l bits + // compute minimum size table less than or equal to l bits + z = g - w; + z = (z > l) ? l : z; // table size upper limit + if((f=1<<(j=k-w))>a+1){ // try a k-w bit table + // too few codes for k-w bit table + f -= a + 1; // deduct codes from patterns left + xp = k; + if(j < z){ + while (++j < z){ // try smaller tables up to z bits + if((f <<= 1) <= c[++xp]) + break; // enough codes to use up j bits + f -= c[xp]; // else deduct codes from patterns + } + } + } + z = 1 << j; // table entries for j-bit table + + // allocate new table + if (hn[0] + z > MANY){ // (note: doesn't matter for fixed) + return Z_DATA_ERROR; // overflow of MANY + } + u[h] = q = /*hp+*/ hn[0]; // DEBUG + hn[0] += z; + + // connect to last table, if there is one + if(h!=0){ + x[h]=i; // save pattern for backing up + r[0]=(byte)j; // bits in this table + r[1]=(byte)l; // bits to dump before this table + j=i>>>(w - l); + r[2] = (int)(q - u[h-1] - j); // offset to this table + System.arraycopy(r, 0, hp, (u[h-1]+j)*3, 3); // connect to last table + } + else{ + t[0] = q; // first table is returned result + } + } + + // set up table entry in r + r[1] = (byte)(k - w); + if (p >= n){ + r[0] = 128 + 64; // out of values--invalid code + } + else if (v[p] < s){ + r[0] = (byte)(v[p] < 256 ? 0 : 32 + 64); // 256 is end-of-block + r[2] = v[p++]; // simple code is just the value + } + else{ + r[0]=(byte)(e[v[p]-s]+16+64); // non-simple--look up in lists + r[2]=d[v[p++] - s]; + } + + // fill code-like entries with r + f=1<<(k-w); + for (j=i>>>w;j>>= 1){ + i ^= j; + } + i ^= j; + + // backup over finished tables + mask = (1 << w) - 1; // needed on HP, cc -O bug + while ((i & mask) != x[h]){ + h--; // don't need to update q + w -= l; + mask = (1 << w) - 1; + } + } + } + // Return Z_BUF_ERROR if we were given an incomplete table + return y != 0 && g != 1 ? Z_BUF_ERROR : Z_OK; + } + + int inflate_trees_bits(int[] c, // 19 code lengths + int[] bb, // bits tree desired/actual depth + int[] tb, // bits tree result + int[] hp, // space for trees + ZStream z // for messages + ){ + int result; + initWorkArea(19); + hn[0]=0; + result = huft_build(c, 0, 19, 19, null, null, tb, bb, hp, hn, v); + + if(result == Z_DATA_ERROR){ + z.msg = "oversubscribed dynamic bit lengths tree"; + } + else if(result == Z_BUF_ERROR || bb[0] == 0){ + z.msg = "incomplete dynamic bit lengths tree"; + result = Z_DATA_ERROR; + } + return result; + } + + int inflate_trees_dynamic(int nl, // number of literal/length codes + int nd, // number of distance codes + int[] c, // that many (total) code lengths + int[] bl, // literal desired/actual bit depth + int[] bd, // distance desired/actual bit depth + int[] tl, // literal/length tree result + int[] td, // distance tree result + int[] hp, // space for trees + ZStream z // for messages + ){ + int result; + + // build literal/length tree + initWorkArea(288); + hn[0]=0; + result = huft_build(c, 0, nl, 257, cplens, cplext, tl, bl, hp, hn, v); + if (result != Z_OK || bl[0] == 0){ + if(result == Z_DATA_ERROR){ + z.msg = "oversubscribed literal/length tree"; + } + else if (result != Z_MEM_ERROR){ + z.msg = "incomplete literal/length tree"; + result = Z_DATA_ERROR; + } + return result; + } + + // build distance tree + initWorkArea(288); + result = huft_build(c, nl, nd, 0, cpdist, cpdext, td, bd, hp, hn, v); + + if (result != Z_OK || (bd[0] == 0 && nl > 257)){ + if (result == Z_DATA_ERROR){ + z.msg = "oversubscribed distance tree"; + } + else if (result == Z_BUF_ERROR) { + z.msg = "incomplete distance tree"; + result = Z_DATA_ERROR; + } + else if (result != Z_MEM_ERROR){ + z.msg = "empty distance tree with lengths"; + result = Z_DATA_ERROR; + } + return result; + } + + return Z_OK; + } + + static int inflate_trees_fixed(int[] bl, //literal desired/actual bit depth + int[] bd, //distance desired/actual bit depth + int[][] tl,//literal/length tree result + int[][] td,//distance tree result + ZStream z //for memory allocation + ){ + bl[0]=fixed_bl; + bd[0]=fixed_bd; + tl[0]=fixed_tl; + td[0]=fixed_td; + return Z_OK; + } + + private void initWorkArea(int vsize){ + if(hn==null){ + hn=new int[1]; + v=new int[vsize]; + c=new int[BMAX+1]; + r=new int[3]; + u=new int[BMAX]; + x=new int[BMAX+1]; + } + if(v.lengthstate); + return Z_OK; + } + + int inflateInit(ZStream z, int w){ + z.msg = null; + blocks = null; + + // handle undocumented nowrap option (no zlib header or check) + nowrap = 0; + if(w < 0){ + w = - w; + nowrap = 1; + } + + // set window size + if(w<8 ||w>15){ + inflateEnd(z); + return Z_STREAM_ERROR; + } + wbits=w; + + z.istate.blocks=new InfBlocks(z, + z.istate.nowrap!=0 ? null : this, + 1<>4)+8>z.istate.wbits){ + z.istate.mode = BAD; + z.msg="invalid window size"; + z.istate.marker = 5; // can't try inflateSync + break; + } + z.istate.mode=FLAG; + case FLAG: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + b = (z.next_in[z.next_in_index++])&0xff; + + if((((z.istate.method << 8)+b) % 31)!=0){ + z.istate.mode = BAD; + z.msg = "incorrect header check"; + z.istate.marker = 5; // can't try inflateSync + break; + } + + if((b&PRESET_DICT)==0){ + z.istate.mode = BLOCKS; + break; + } + z.istate.mode = DICT4; + case DICT4: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need=((z.next_in[z.next_in_index++]&0xff)<<24)&0xff000000L; + z.istate.mode=DICT3; + case DICT3: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need+=((z.next_in[z.next_in_index++]&0xff)<<16)&0xff0000L; + z.istate.mode=DICT2; + case DICT2: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need+=((z.next_in[z.next_in_index++]&0xff)<<8)&0xff00L; + z.istate.mode=DICT1; + case DICT1: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need += (z.next_in[z.next_in_index++]&0xffL); + z.adler = z.istate.need; + z.istate.mode = DICT0; + return Z_NEED_DICT; + case DICT0: + z.istate.mode = BAD; + z.msg = "need dictionary"; + z.istate.marker = 0; // can try inflateSync + return Z_STREAM_ERROR; + case BLOCKS: + + r = z.istate.blocks.proc(z, r); + if(r == Z_DATA_ERROR){ + z.istate.mode = BAD; + z.istate.marker = 0; // can try inflateSync + break; + } + if(r == Z_OK){ + r = f; + } + if(r != Z_STREAM_END){ + return r; + } + r = f; + z.istate.blocks.reset(z, z.istate.was); + if(z.istate.nowrap!=0){ + z.istate.mode=DONE; + break; + } + z.istate.mode=CHECK4; + case CHECK4: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need=((z.next_in[z.next_in_index++]&0xff)<<24)&0xff000000L; + z.istate.mode=CHECK3; + case CHECK3: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need+=((z.next_in[z.next_in_index++]&0xff)<<16)&0xff0000L; + z.istate.mode = CHECK2; + case CHECK2: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need+=((z.next_in[z.next_in_index++]&0xff)<<8)&0xff00L; + z.istate.mode = CHECK1; + case CHECK1: + + if(z.avail_in==0)return r;r=f; + + z.avail_in--; z.total_in++; + z.istate.need+=(z.next_in[z.next_in_index++]&0xffL); + + if(((int)(z.istate.was[0])) != ((int)(z.istate.need))){ + z.istate.mode = BAD; + z.msg = "incorrect data check"; + z.istate.marker = 5; // can't try inflateSync + break; + } + + z.istate.mode = DONE; + case DONE: + return Z_STREAM_END; + case BAD: + return Z_DATA_ERROR; + default: + return Z_STREAM_ERROR; + } + } + } + + + int inflateSetDictionary(ZStream z, byte[] dictionary, int dictLength){ + int index=0; + int length = dictLength; + if(z==null || z.istate == null|| z.istate.mode != DICT0) + return Z_STREAM_ERROR; + + if(z._adler.adler32(1L, dictionary, 0, dictLength)!=z.adler){ + return Z_DATA_ERROR; + } + + z.adler = z._adler.adler32(0, null, 0, 0); + + if(length >= (1<>>7)]); + } + + short[] dyn_tree; // the dynamic tree + int max_code; // largest code with non zero frequency + StaticTree stat_desc; // the corresponding static tree + + // Compute the optimal bit lengths for a tree and update the total bit length + // for the current block. + // IN assertion: the fields freq and dad are set, heap[heap_max] and + // above are the tree nodes sorted by increasing frequency. + // OUT assertions: the field len is set to the optimal bit length, the + // array bl_count contains the frequencies for each bit length. + // The length opt_len is updated; static_len is also updated if stree is + // not null. + void gen_bitlen(Deflate s){ + short[] tree = dyn_tree; + short[] stree = stat_desc.static_tree; + int[] extra = stat_desc.extra_bits; + int base = stat_desc.extra_base; + int max_length = stat_desc.max_length; + int h; // heap index + int n, m; // iterate over the tree elements + int bits; // bit length + int xbits; // extra bits + short f; // frequency + int overflow = 0; // number of elements with bit length too large + + for (bits = 0; bits <= MAX_BITS; bits++) s.bl_count[bits] = 0; + + // In a first pass, compute the optimal bit lengths (which may + // overflow in the case of the bit length tree). + tree[s.heap[s.heap_max]*2+1] = 0; // root of the heap + + for(h=s.heap_max+1; h max_length){ bits = max_length; overflow++; } + tree[n*2+1] = (short)bits; + // We overwrite tree[n*2+1] which is no longer needed + + if (n > max_code) continue; // not a leaf node + + s.bl_count[bits]++; + xbits = 0; + if (n >= base) xbits = extra[n-base]; + f = tree[n*2]; + s.opt_len += f * (bits + xbits); + if (stree!=null) s.static_len += f * (stree[n*2+1] + xbits); + } + if (overflow == 0) return; + + // This happens for example on obj2 and pic of the Calgary corpus + // Find the first bit length which could increase: + do { + bits = max_length-1; + while(s.bl_count[bits]==0) bits--; + s.bl_count[bits]--; // move one leaf down the tree + s.bl_count[bits+1]+=2; // move one overflow item as its brother + s.bl_count[max_length]--; + // The brother of the overflow item also moves one step up, + // but this does not affect bl_count[max_length] + overflow -= 2; + } + while (overflow > 0); + + for (bits = max_length; bits != 0; bits--) { + n = s.bl_count[bits]; + while (n != 0) { + m = s.heap[--h]; + if (m > max_code) continue; + if (tree[m*2+1] != bits) { + s.opt_len += ((long)bits - (long)tree[m*2+1])*(long)tree[m*2]; + tree[m*2+1] = (short)bits; + } + n--; + } + } + } + + // Construct one Huffman tree and assigns the code bit strings and lengths. + // Update the total bit length for the current block. + // IN assertion: the field freq is set for all tree elements. + // OUT assertions: the fields len and code are set to the optimal bit length + // and corresponding code. The length opt_len is updated; static_len is + // also updated if stree is not null. The field max_code is set. + void build_tree(Deflate s){ + short[] tree=dyn_tree; + short[] stree=stat_desc.static_tree; + int elems=stat_desc.elems; + int n, m; // iterate over heap elements + int max_code=-1; // largest code with non zero frequency + int node; // new node being created + + // Construct the initial heap, with least frequent element in + // heap[1]. The sons of heap[n] are heap[2*n] and heap[2*n+1]. + // heap[0] is not used. + s.heap_len = 0; + s.heap_max = HEAP_SIZE; + + for(n=0; n=1; n--) + s.pqdownheap(tree, n); + + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + + node=elems; // next internal node of the tree + do{ + // n = node of least frequency + n=s.heap[1]; + s.heap[1]=s.heap[s.heap_len--]; + s.pqdownheap(tree, 1); + m=s.heap[1]; // m = node of next least frequency + + s.heap[--s.heap_max] = n; // keep the nodes sorted by frequency + s.heap[--s.heap_max] = m; + + // Create a new node father of n and m + tree[node*2] = (short)(tree[n*2] + tree[m*2]); + s.depth[node] = (byte)(Math.max(s.depth[n],s.depth[m])+1); + tree[n*2+1] = tree[m*2+1] = (short)node; + + // and insert the new node in the heap + s.heap[1] = node++; + s.pqdownheap(tree, 1); + } + while(s.heap_len>=2); + + s.heap[--s.heap_max] = s.heap[1]; + + // At this point, the fields freq and dad are set. We can now + // generate the bit lengths. + + gen_bitlen(s); + + // The field len is now set, we can generate the bit codes + gen_codes(tree, max_code, s.bl_count); + } + + // Generate the codes for a given tree and bit counts (which need not be + // optimal). + // IN assertion: the array bl_count contains the bit length statistics for + // the given tree and the field len is set for all tree elements. + // OUT assertion: the field code is set for all tree elements of non + // zero code length. + static void gen_codes(short[] tree, // the tree to decorate + int max_code, // largest code with non zero frequency + short[] bl_count // number of codes at each bit length + ){ + short[] next_code=new short[MAX_BITS+1]; // next code value for each bit length + short code = 0; // running code value + int bits; // bit index + int n; // code index + + // The distribution counts are first used to generate the code values + // without bit reversal. + for (bits = 1; bits <= MAX_BITS; bits++) { + next_code[bits] = code = (short)((code + bl_count[bits-1]) << 1); + } + + // Check that the bit counts in bl_count are consistent. The last code + // must be all ones. + //Assert (code + bl_count[MAX_BITS]-1 == (1<>>=1; + res<<=1; + } + while(--len>0); + return res>>>1; + } +} + diff --git a/app/src/main/java/com/jcraft/jzlib/ZInputStream.java b/app/src/main/java/com/jcraft/jzlib/ZInputStream.java new file mode 100644 index 0000000..3f97c44 --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/ZInputStream.java @@ -0,0 +1,149 @@ +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2001 Lapo Luchini. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS +OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; +import java.io.*; + +public class ZInputStream extends FilterInputStream { + + protected ZStream z=new ZStream(); + protected int bufsize=512; + protected int flush=JZlib.Z_NO_FLUSH; + protected byte[] buf=new byte[bufsize], + buf1=new byte[1]; + protected boolean compress; + + protected InputStream in=null; + + public ZInputStream(InputStream in) { + this(in, false); + } + public ZInputStream(InputStream in, boolean nowrap) { + super(in); + this.in=in; + z.inflateInit(nowrap); + compress=false; + z.next_in=buf; + z.next_in_index=0; + z.avail_in=0; + } + + public ZInputStream(InputStream in, int level) { + super(in); + this.in=in; + z.deflateInit(level); + compress=true; + z.next_in=buf; + z.next_in_index=0; + z.avail_in=0; + } + + /*public int available() throws IOException { + return inf.finished() ? 0 : 1; + }*/ + + public int read() throws IOException { + if(read(buf1, 0, 1)==-1) + return(-1); + return(buf1[0]&0xFF); + } + + private boolean nomoreinput=false; + + public int read(byte[] b, int off, int len) throws IOException { + if(len==0) + return(0); + int err; + z.next_out=b; + z.next_out_index=off; + z.avail_out=len; + do { + if((z.avail_in==0)&&(!nomoreinput)) { // if buffer is empty and more input is avaiable, refill it + z.next_in_index=0; + z.avail_in=in.read(buf, 0, bufsize);//(bufsize0 || z.avail_out==0); + } + + public int getFlushMode() { + return(flush); + } + + public void setFlushMode(int flush) { + this.flush=flush; + } + + public void finish() throws IOException { + int err; + do{ + z.next_out=buf; + z.next_out_index=0; + z.avail_out=bufsize; + if(compress){ err=z.deflate(JZlib.Z_FINISH); } + else{ err=z.inflate(JZlib.Z_FINISH); } + if(err!=JZlib.Z_STREAM_END && err != JZlib.Z_OK) + throw new ZStreamException((compress?"de":"in")+"flating: "+z.msg); + if(bufsize-z.avail_out>0){ + out.write(buf, 0, bufsize-z.avail_out); + } + } + while(z.avail_in>0 || z.avail_out==0); + flush(); + } + public void end() { + if(z==null) + return; + if(compress){ z.deflateEnd(); } + else{ z.inflateEnd(); } + z.free(); + z=null; + } + public void close() throws IOException { + try{ + try{finish();} + catch (IOException ignored) {} + } + finally{ + end(); + out.close(); + out=null; + } + } + + /** + * Returns the total number of bytes input so far. + */ + public long getTotalIn() { + return z.total_in; + } + + /** + * Returns the total number of bytes output so far. + */ + public long getTotalOut() { + return z.total_out; + } + + public void flush() throws IOException { + out.flush(); + } + +} diff --git a/app/src/main/java/com/jcraft/jzlib/ZStream.java b/app/src/main/java/com/jcraft/jzlib/ZStream.java new file mode 100644 index 0000000..334475e --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/ZStream.java @@ -0,0 +1,211 @@ +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +final public class ZStream{ + + static final private int MAX_WBITS=15; // 32K LZ77 window + static final private int DEF_WBITS=MAX_WBITS; + + static final private int Z_NO_FLUSH=0; + static final private int Z_PARTIAL_FLUSH=1; + static final private int Z_SYNC_FLUSH=2; + static final private int Z_FULL_FLUSH=3; + static final private int Z_FINISH=4; + + static final private int MAX_MEM_LEVEL=9; + + static final private int Z_OK=0; + static final private int Z_STREAM_END=1; + static final private int Z_NEED_DICT=2; + static final private int Z_ERRNO=-1; + static final private int Z_STREAM_ERROR=-2; + static final private int Z_DATA_ERROR=-3; + static final private int Z_MEM_ERROR=-4; + static final private int Z_BUF_ERROR=-5; + static final private int Z_VERSION_ERROR=-6; + + public byte[] next_in; // next input byte + public int next_in_index; + public int avail_in; // number of bytes available at next_in + public long total_in; // total nb of input bytes read so far + + public byte[] next_out; // next output byte should be put there + public int next_out_index; + public int avail_out; // remaining free space at next_out + public long total_out; // total nb of bytes output so far + + public String msg; + + Deflate dstate; + Inflate istate; + + int data_type; // best guess about the data type: ascii or binary + + public long adler; + Adler32 _adler=new Adler32(); + + public int inflateInit(){ + return inflateInit(DEF_WBITS); + } + public int inflateInit(boolean nowrap){ + return inflateInit(DEF_WBITS, nowrap); + } + public int inflateInit(int w){ + return inflateInit(w, false); + } + + public int inflateInit(int w, boolean nowrap){ + istate=new Inflate(); + return istate.inflateInit(this, nowrap?-w:w); + } + + public int inflate(int f){ + if(istate==null) return Z_STREAM_ERROR; + return istate.inflate(this, f); + } + public int inflateEnd(){ + if(istate==null) return Z_STREAM_ERROR; + int ret=istate.inflateEnd(this); + istate = null; + return ret; + } + public int inflateSync(){ + if(istate == null) + return Z_STREAM_ERROR; + return istate.inflateSync(this); + } + public int inflateSetDictionary(byte[] dictionary, int dictLength){ + if(istate == null) + return Z_STREAM_ERROR; + return istate.inflateSetDictionary(this, dictionary, dictLength); + } + + public int deflateInit(int level){ + return deflateInit(level, MAX_WBITS); + } + public int deflateInit(int level, boolean nowrap){ + return deflateInit(level, MAX_WBITS, nowrap); + } + public int deflateInit(int level, int bits){ + return deflateInit(level, bits, false); + } + public int deflateInit(int level, int bits, boolean nowrap){ + dstate=new Deflate(); + return dstate.deflateInit(this, level, nowrap?-bits:bits); + } + public int deflate(int flush){ + if(dstate==null){ + return Z_STREAM_ERROR; + } + return dstate.deflate(this, flush); + } + public int deflateEnd(){ + if(dstate==null) return Z_STREAM_ERROR; + int ret=dstate.deflateEnd(); + dstate=null; + return ret; + } + public int deflateParams(int level, int strategy){ + if(dstate==null) return Z_STREAM_ERROR; + return dstate.deflateParams(this, level, strategy); + } + public int deflateSetDictionary (byte[] dictionary, int dictLength){ + if(dstate == null) + return Z_STREAM_ERROR; + return dstate.deflateSetDictionary(this, dictionary, dictLength); + } + + // Flush as much pending output as possible. All deflate() output goes + // through this function so some applications may wish to modify it + // to avoid allocating a large strm->next_out buffer and copying into it. + // (See also read_buf()). + void flush_pending(){ + int len=dstate.pending; + + if(len>avail_out) len=avail_out; + if(len==0) return; + + if(dstate.pending_buf.length<=dstate.pending_out || + next_out.length<=next_out_index || + dstate.pending_buf.length<(dstate.pending_out+len) || + next_out.length<(next_out_index+len)){ + System.out.println(dstate.pending_buf.length+", "+dstate.pending_out+ + ", "+next_out.length+", "+next_out_index+", "+len); + System.out.println("avail_out="+avail_out); + } + + System.arraycopy(dstate.pending_buf, dstate.pending_out, + next_out, next_out_index, len); + + next_out_index+=len; + dstate.pending_out+=len; + total_out+=len; + avail_out-=len; + dstate.pending-=len; + if(dstate.pending==0){ + dstate.pending_out=0; + } + } + + // Read a new buffer from the current input stream, update the adler32 + // and total number of bytes read. All deflate() input goes through + // this function so some applications may wish to modify it to avoid + // allocating a large strm->next_in buffer and copying from it. + // (See also flush_pending()). + int read_buf(byte[] buf, int start, int size) { + int len=avail_in; + + if(len>size) len=size; + if(len==0) return 0; + + avail_in-=len; + + if(dstate.noheader==0) { + adler=_adler.adler32(adler, next_in, next_in_index, len); + } + System.arraycopy(next_in, next_in_index, buf, start, len); + next_in_index += len; + total_in += len; + return len; + } + + public void free(){ + next_in=null; + next_out=null; + msg=null; + _adler=null; + } +} diff --git a/app/src/main/java/com/jcraft/jzlib/ZStreamException.java b/app/src/main/java/com/jcraft/jzlib/ZStreamException.java new file mode 100644 index 0000000..308bb8a --- /dev/null +++ b/app/src/main/java/com/jcraft/jzlib/ZStreamException.java @@ -0,0 +1,44 @@ +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.jcraft.jzlib; + +public class ZStreamException extends java.io.IOException { + public ZStreamException() { + super(); + } + public ZStreamException(String s) { + super(s); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/AuthAgentCallback.java b/app/src/main/java/com/trilead/ssh2/AuthAgentCallback.java new file mode 100644 index 0000000..7fe270b --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/AuthAgentCallback.java @@ -0,0 +1,64 @@ +package com.trilead.ssh2; + +import java.security.KeyPair; +import java.util.Map; + +/** + * AuthAgentCallback. + * + * @author Kenny Root + * @version $Id$ + */ +public interface AuthAgentCallback { + + /** + * @return array of blobs containing the OpenSSH-format encoded public keys + */ + Map retrieveIdentities(); + + /** + * @param key A RSAPrivateKey or DSAPrivateKey + * containing a DSA or RSA private key of + * the user in Trilead object format. + * @param comment comment associated with this key + * @param confirmUse whether to prompt before using this key + * @param lifetime lifetime in seconds for key to be remembered + * @return success or failure + */ + boolean addIdentity(KeyPair pair, String comment, boolean confirmUse, int lifetime); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return success or failure + */ + boolean removeIdentity(byte[] publicKey); + + /** + * @return success or failure + */ + boolean removeAllIdentities(); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return A RSAPrivateKey or DSAPrivateKey + * containing a DSA or RSA private key of + * the user in Trilead object format. + */ + KeyPair getKeyPair(byte[] publicKey); + + /** + * @return + */ + boolean isAgentLocked(); + + /** + * @param lockPassphrase + */ + boolean setAgentLock(String lockPassphrase); + + /** + * @param unlockPassphrase + * @return + */ + boolean requestAgentUnlock(String unlockPassphrase); +} diff --git a/app/src/main/java/com/trilead/ssh2/ChannelCondition.java b/app/src/main/java/com/trilead/ssh2/ChannelCondition.java new file mode 100644 index 0000000..df1ad50 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/ChannelCondition.java @@ -0,0 +1,61 @@ + +package com.trilead.ssh2; + +/** + * Contains constants that can be used to specify what conditions to wait for on + * a SSH-2 channel (e.g., represented by a {@link Session}). + * + * @see Session#waitForCondition(int, long) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelCondition.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public abstract interface ChannelCondition +{ + /** + * A timeout has occurred, none of your requested conditions is fulfilled. + * However, other conditions may be true - therefore, NEVER use the "==" + * operator to test for this (or any other) condition. Always use + * something like ((cond & ChannelCondition.CLOSED) != 0). + */ + public static final int TIMEOUT = 1; + + /** + * The underlying SSH-2 channel, however not necessarily the whole connection, + * has been closed. This implies EOF. Note that there may still + * be unread stdout or stderr data in the local window, i.e, STDOUT_DATA + * or/and STDERR_DATA may be set at the same time. + */ + public static final int CLOSED = 2; + + /** + * There is stdout data available that is ready to be consumed. + */ + public static final int STDOUT_DATA = 4; + + /** + * There is stderr data available that is ready to be consumed. + */ + public static final int STDERR_DATA = 8; + + /** + * EOF on has been reached, no more _new_ stdout or stderr data will arrive + * from the remote server. However, there may be unread stdout or stderr + * data, i.e, STDOUT_DATA or/and STDERR_DATA + * may be set at the same time. + */ + public static final int EOF = 16; + + /** + * The exit status of the remote process is available. + * Some servers never send the exist status, or occasionally "forget" to do so. + */ + public static final int EXIT_STATUS = 32; + + /** + * The exit signal of the remote process is available. + */ + public static final int EXIT_SIGNAL = 64; + +} diff --git a/app/src/main/java/com/trilead/ssh2/Connection.java b/app/src/main/java/com/trilead/ssh2/Connection.java new file mode 100644 index 0000000..163fdb5 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/Connection.java @@ -0,0 +1,1628 @@ + +package com.trilead.ssh2; + +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Set; +import java.util.Vector; + +import com.trilead.ssh2.auth.AuthenticationManager; +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketIgnore; +import com.trilead.ssh2.transport.KexManager; +import com.trilead.ssh2.transport.TransportManager; +import com.trilead.ssh2.util.TimeoutService; +import com.trilead.ssh2.util.TimeoutService.TimeoutToken; + +/** + * A Connection is used to establish an encrypted TCP/IP + * connection to a SSH-2 server. + *

+ * Typically, one + *

    + *
  1. creates a {@link #Connection(String) Connection} object.
  2. + *
  3. calls the {@link #connect() connect()} method.
  4. + *
  5. calls some of the authentication methods (e.g., + * {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()}).
  6. + *
  7. calls one or several times the {@link #openSession() openSession()} + * method.
  8. + *
  9. finally, one must close the connection and release resources with the + * {@link #close() close()} method.
  10. + *
+ * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Connection.java,v 1.3 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class Connection +{ + /** + * The identifier presented to the SSH-2 server. + */ + public final static String identification = "TrileadSSH2Java_213"; + + /** + * Will be used to generate all random data needed for the current + * connection. Note: SecureRandom.nextBytes() is thread safe. + */ + private SecureRandom generator; + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported cipher algorithms by this implementation. + */ + public static synchronized String[] getAvailableCiphers() + { + return BlockCipherFactory.getDefaultCipherList(); + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported MAC algorthims by this implementation. + */ + public static synchronized String[] getAvailableMACs() + { + return MAC.getMacList(); + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported server host key algorthims by this + * implementation. + */ + public static synchronized String[] getAvailableServerHostKeyAlgorithms() + { + return KexManager.getDefaultServerHostkeyAlgorithmList(); + } + + private AuthenticationManager am; + + private boolean authenticated = false; + private boolean compression = false; + private ChannelManager cm; + + private CryptoWishList cryptoWishList = new CryptoWishList(); + + private DHGexParameters dhgexpara = new DHGexParameters(); + + private final String hostname; + + private final int port; + + private TransportManager tm; + + private boolean tcpNoDelay = false; + + private ProxyData proxyData = null; + + private Vector connectionMonitors = new Vector(); + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + *

+ * Same as {@link #Connection(String, int) Connection(hostname, 22)}. + * + * @param hostname + * the hostname of the SSH-2 server. + */ + public Connection(String hostname) + { + this(hostname, 22); + } + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + * + * @param hostname + * the host where we later want to connect to. + * @param port + * port on the server, normally 22. + */ + public Connection(String hostname, int port) + { + this.hostname = hostname; + this.port = port; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * is based on DSA (it uses DSA to sign a challenge sent by the server). + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + * + * @param user + * A String holding the username. + * @param pem + * A String containing the DSA private key of the + * user in OpenSSH key format (PEM, you can't miss the + * "-----BEGIN DSA PRIVATE KEY-----" tag). The string may contain + * linefeeds. + * @param password + * If the PEM string is 3DES encrypted ("DES-EDE3-CBC"), then you + * must specify the password. Otherwise, this argument will be + * ignored and can be set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + * + * @deprecated You should use one of the + * {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()} + * methods, this method is just a wrapper for it and will + * disappear in future builds. + * + */ + public synchronized boolean authenticateWithDSA(String user, String pem, String password) throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (pem == null) + throw new IllegalArgumentException("pem argument is null"); + + authenticated = am.authenticatePublicKey(user, pem.toCharArray(), password, getOrCreateSecureRND()); + + return authenticated; + } + + /** + * A wrapper that calls + * {@link #authenticateWithKeyboardInteractive(String, String[], InteractiveCallback) + * authenticateWithKeyboardInteractivewith} a null submethod + * list. + * + * @param user + * A String holding the username. + * @param cb + * An InteractiveCallback which will be used to + * determine the responses to the questions asked by the server. + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithKeyboardInteractive(String user, InteractiveCallback cb) + throws IOException + { + return authenticateWithKeyboardInteractive(user, null, cb); + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * is based on "keyboard-interactive", specified in + * draft-ietf-secsh-auth-kbdinteract-XX. Basically, you have to define a + * callback object which will be feeded with challenges generated by the + * server. Answers are then sent back to the server. It is possible that the + * callback will be called several times during the invocation of this + * method (e.g., if the server replies to the callback's answer(s) with + * another challenge...) + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * Note: some SSH servers advertise "keyboard-interactive", however, any + * interactive request will be denied (without having sent any challenge to + * the client). + * + * @param user + * A String holding the username. + * @param submethods + * An array of submethod names, see + * draft-ietf-secsh-auth-kbdinteract-XX. May be null + * to indicate an empty list. + * @param cb + * An InteractiveCallback which will be used to + * determine the responses to the questions asked by the server. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithKeyboardInteractive(String user, String[] submethods, + InteractiveCallback cb) throws IOException + { + if (cb == null) + throw new IllegalArgumentException("Callback may not ne NULL!"); + + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + authenticated = am.authenticateInteractive(user, submethods, cb); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * sends username and password to the server. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * Note: if this method fails, then please double-check that it is actually + * offered by the server (use + * {@link #getRemainingAuthMethods(String) getRemainingAuthMethods()}. + *

+ * Often, password authentication is disabled, but users are not aware of + * it. Many servers only offer "publickey" and "keyboard-interactive". + * However, even though "keyboard-interactive" *feels* like password + * authentication (e.g., when using the putty or openssh clients) it is + * *not* the same mechanism. + * + * @param user + * @param password + * @return if the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPassword(String user, String password) throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (password == null) + throw new IllegalArgumentException("password argument is null"); + + authenticated = am.authenticatePassword(user, password); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * can be used to explicitly use the special "none" authentication method + * (where only a username has to be specified). + *

+ * Note 1: The "none" method may always be tried by clients, however as by + * the specs, the server will not explicitly announce it. In other words, + * the "none" token will never show up in the list returned by + * {@link #getRemainingAuthMethods(String)}. + *

+ * Note 2: no matter which one of the authenticateWithXXX() methods you + * call, the library will always issue exactly one initial "none" + * authentication request to retrieve the initially allowed list of + * authentication methods by the server. Please read RFC 4252 for the + * details. + *

+ * If the authentication phase is complete, true will be + * returned. If further authentication steps are needed, false + * is returned and one can retry by any other authentication method (use the + * getRemainingAuthMethods method to get a list of the + * remaining possible methods). + * + * @param user + * @return if the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithNone(String user) throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + /* Trigger the sending of the PacketUserauthRequestNone packet */ + /* (if not already done) */ + + authenticated = am.authenticateNone(user); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. The + * authentication method "publickey" works by signing a challenge sent by + * the server. The signature is either DSA or RSA based - it just depends on + * the type of private key you specify, either a DSA or RSA private key in + * PEM format. And yes, this is may seem to be a little confusing, the + * method is called "publickey" in the SSH-2 protocol specification, however + * since we need to generate a signature, you actually have to supply a + * private key =). + *

+ * The private key contained in the PEM file may also be encrypted + * ("Proc-Type: 4,ENCRYPTED"). The library supports DES-CBC and DES-EDE3-CBC + * encryption, as well as the more exotic PEM encrpytions AES-128-CBC, + * AES-192-CBC and AES-256-CBC. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * NOTE PUTTY USERS: Event though your key file may start with + * "-----BEGIN..." it is not in the expected format. You have to convert it + * to the OpenSSH key format by using the "puttygen" tool (can be downloaded + * from the Putty website). Simply load your key and then use the + * "Conversions/Export OpenSSH key" functionality to get a proper PEM file. + * + * @param user + * A String holding the username. + * @param pemPrivateKey + * A char[] containing a DSA or RSA private key of + * the user in OpenSSH key format (PEM, you can't miss the + * "-----BEGIN DSA PRIVATE KEY-----" or "-----BEGIN RSA PRIVATE + * KEY-----" tag). The char array may contain + * linebreaks/linefeeds. + * @param password + * If the PEM structure is encrypted ("Proc-Type: 4,ENCRYPTED") + * then you must specify a password. Otherwise, this argument + * will be ignored and can be set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, char[] pemPrivateKey, String password) + throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (pemPrivateKey == null) + throw new IllegalArgumentException("pemPrivateKey argument is null"); + + authenticated = am.authenticatePublicKey(user, pemPrivateKey, password, getOrCreateSecureRND()); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. The + * authentication method "publickey" works by signing a challenge sent by + * the server. The signature is either DSA or RSA based - it just depends on + * the type of private key you specify, either a DSA or RSA private key in + * PEM format. And yes, this is may seem to be a little confusing, the + * method is called "publickey" in the SSH-2 protocol specification, however + * since we need to generate a signature, you actually have to supply a + * private key =). + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + * + * @param user + * A String holding the username. + * @param key + * A RSAPrivateKey or DSAPrivateKey + * containing a DSA or RSA private key of + * the user in Trilead object format. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, KeyPair pair) + throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (pair == null) + throw new IllegalArgumentException("Key pair argument is null"); + + authenticated = am.authenticatePublicKey(user, pair, getOrCreateSecureRND()); + + return authenticated; + } + /** + * A convenience wrapper function which reads in a private key (PEM format, + * either DSA or RSA) and then calls + * authenticateWithPublicKey(String, char[], String). + *

+ * NOTE PUTTY USERS: Event though your key file may start with + * "-----BEGIN..." it is not in the expected format. You have to convert it + * to the OpenSSH key format by using the "puttygen" tool (can be downloaded + * from the Putty website). Simply load your key and then use the + * "Conversions/Export OpenSSH key" functionality to get a proper PEM file. + * + * @param user + * A String holding the username. + * @param pemFile + * A File object pointing to a file containing a + * DSA or RSA private key of the user in OpenSSH key format (PEM, + * you can't miss the "-----BEGIN DSA PRIVATE KEY-----" or + * "-----BEGIN RSA PRIVATE KEY-----" tag). + * @param password + * If the PEM file is encrypted then you must specify the + * password. Otherwise, this argument will be ignored and can be + * set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, File pemFile, String password) + throws IOException + { + if (pemFile == null) + throw new IllegalArgumentException("pemFile argument is null"); + + char[] buff = new char[256]; + + CharArrayWriter cw = new CharArrayWriter(); + + FileReader fr = new FileReader(pemFile); + + while (true) + { + int len = fr.read(buff); + if (len < 0) + break; + cw.write(buff, 0, len); + } + + fr.close(); + + return authenticateWithPublicKey(user, cw.toCharArray(), password); + } + + /** + * Add a {@link ConnectionMonitor} to this connection. Can be invoked at any + * time, but it is best to add connection monitors before invoking + * connect() to avoid glitches (e.g., you add a connection + * monitor after a successful connect(), but the connection has died in the + * mean time. Then, your connection monitor won't be notified.) + *

+ * You can add as many monitors as you like. + * + * @see ConnectionMonitor + * + * @param cmon + * An object implementing the ConnectionMonitor + * interface. + */ + public synchronized void addConnectionMonitor(ConnectionMonitor cmon) + { + if (cmon == null) + throw new IllegalArgumentException("cmon argument is null"); + + connectionMonitors.addElement(cmon); + + if (tm != null) + tm.setConnectionMonitors(connectionMonitors); + } + + /** + * Controls whether compression is used on the link or not. + *

+ * Note: This can only be called before connect() + * @param enabled whether to enable compression + * @throws IOException + */ + public synchronized void setCompression(boolean enabled) throws IOException { + if (tm != null) + throw new IOException("Connection to " + hostname + " is already in connected state!"); + + compression = enabled; + } + + /** + * Close the connection to the SSH-2 server. All assigned sessions will be + * closed, too. Can be called at any time. Don't forget to call this once + * you don't need a connection anymore - otherwise the receiver thread may + * run forever. + */ + public synchronized void close() + { + Throwable t = new Throwable("Closed due to user request."); + close(t, false); + } + + private void close(Throwable t, boolean hard) + { + if (cm != null) + cm.closeAllChannels(); + + if (tm != null) + { + tm.close(t, hard == false); + tm = null; + } + am = null; + cm = null; + authenticated = false; + } + + /** + * Same as + * {@link #connect(ServerHostKeyVerifier, int, int) connect(null, 0, 0)}. + * + * @return see comments for the + * {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} + * method. + * @throws IOException + */ + public synchronized ConnectionInfo connect() throws IOException + { + return connect(null, 0, 0); + } + + /** + * Same as + * {@link #connect(ServerHostKeyVerifier, int, int) connect(verifier, 0, 0)}. + * + * @return see comments for the + * {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} + * method. + * @throws IOException + */ + public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier) throws IOException + { + return connect(verifier, 0, 0); + } + + /** + * Connect to the SSH-2 server and, as soon as the server has presented its + * host key, use the + * {@link ServerHostKeyVerifier#verifyServerHostKey(String, int, String, + * byte[]) ServerHostKeyVerifier.verifyServerHostKey()} method of the + * verifier to ask for permission to proceed. If + * verifier is null, then any host key will + * be accepted - this is NOT recommended, since it makes man-in-the-middle + * attackes VERY easy (somebody could put a proxy SSH server between you and + * the real server). + *

+ * Note: The verifier will be called before doing any crypto calculations + * (i.e., diffie-hellman). Therefore, if you don't like the presented host + * key then no CPU cycles are wasted (and the evil server has less + * information about us). + *

+ * However, it is still possible that the server presented a fake host key: + * the server cheated (typically a sign for a man-in-the-middle attack) and + * is not able to generate a signature that matches its host key. Don't + * worry, the library will detect such a scenario later when checking the + * signature (the signature cannot be checked before having completed the + * diffie-hellman exchange). + *

+ * Note 2: The {@link ServerHostKeyVerifier#verifyServerHostKey(String, int, + * String, byte[]) ServerHostKeyVerifier.verifyServerHostKey()} method will + * *NOT* be called from the current thread, the call is being made from a + * background thread (there is a background dispatcher thread for every + * established connection). + *

+ * Note 3: This method will block as long as the key exchange of the + * underlying connection has not been completed (and you have not specified + * any timeouts). + *

+ * Note 4: If you want to re-use a connection object that was successfully + * connected, then you must call the {@link #close()} method before invoking + * connect() again. + * + * @param verifier + * An object that implements the {@link ServerHostKeyVerifier} + * interface. Pass null to accept any server host + * key - NOT recommended. + * + * @param connectTimeout + * Connect the underlying TCP socket to the server with the given + * timeout value (non-negative, in milliseconds). Zero means no + * timeout. If a proxy is being used (see + * {@link #setProxyData(ProxyData)}), then this timeout is used + * for the connection establishment to the proxy. + * + * @param kexTimeout + * Timeout for complete connection establishment (non-negative, + * in milliseconds). Zero means no timeout. The timeout counts + * from the moment you invoke the connect() method and is + * cancelled as soon as the first key-exchange round has + * finished. It is possible that the timeout event will be fired + * during the invocation of the verifier callback, + * but it will only have an effect after the + * verifier returns. + * + * @return A {@link ConnectionInfo} object containing the details of the + * established connection. + * + * @throws IOException + * If any problem occurs, e.g., the server's host key is not + * accepted by the verifier or there is problem + * during the initial crypto setup (e.g., the signature sent by + * the server is wrong). + *

+ * In case of a timeout (either connectTimeout or kexTimeout) a + * SocketTimeoutException is thrown. + *

+ * An exception may also be thrown if the connection was already + * successfully connected (no matter if the connection broke in + * the mean time) and you invoke connect() again + * without having called {@link #close()} first. + *

+ * If a HTTP proxy is being used and the proxy refuses the + * connection, then a {@link HTTPProxyException} may be thrown, + * which contains the details returned by the proxy. If the + * proxy is buggy and does not return a proper HTTP response, + * then a normal IOException is thrown instead. + */ + public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier, int connectTimeout, int kexTimeout) + throws IOException + { + final class TimeoutState + { + boolean isCancelled = false; + boolean timeoutSocketClosed = false; + } + + if (tm != null) + throw new IOException("Connection to " + hostname + " is already in connected state!"); + + if (connectTimeout < 0) + throw new IllegalArgumentException("connectTimeout must be non-negative!"); + + if (kexTimeout < 0) + throw new IllegalArgumentException("kexTimeout must be non-negative!"); + + final TimeoutState state = new TimeoutState(); + + tm = new TransportManager(hostname, port); + + tm.setConnectionMonitors(connectionMonitors); + + // Don't offer compression if not requested + if (!compression) { + cryptoWishList.c2s_comp_algos = new String[] { "none" }; + cryptoWishList.s2c_comp_algos = new String[] { "none" }; + } + + /* + * Make sure that the runnable below will observe the new value of "tm" + * and "state" (the runnable will be executed in a different thread, + * which may be already running, that is why we need a memory barrier + * here). See also the comment in Channel.java if you are interested in + * the details. + * + * OKOK, this is paranoid since adding the runnable to the todo list of + * the TimeoutService will ensure that all writes have been flushed + * before the Runnable reads anything (there is a synchronized block in + * TimeoutService.addTimeoutHandler). + */ + + synchronized (tm) + { + /* We could actually synchronize on anything. */ + } + + try + { + TimeoutToken token = null; + + if (kexTimeout > 0) + { + final Runnable timeoutHandler = new Runnable() + { + public void run() + { + synchronized (state) + { + if (state.isCancelled) + return; + state.timeoutSocketClosed = true; + tm.close(new SocketTimeoutException("The connect timeout expired"), false); + } + } + }; + + long timeoutHorizont = System.currentTimeMillis() + kexTimeout; + + token = TimeoutService.addTimeoutHandler(timeoutHorizont, timeoutHandler); + } + + try + { + tm.initialize(cryptoWishList, verifier, dhgexpara, connectTimeout, getOrCreateSecureRND(), proxyData); + } + catch (SocketTimeoutException se) + { + throw (SocketTimeoutException) new SocketTimeoutException( + "The connect() operation on the socket timed out.").initCause(se); + } + + tm.setTcpNoDelay(tcpNoDelay); + + /* Wait until first KEX has finished */ + + ConnectionInfo ci = tm.getConnectionInfo(1); + + /* Now try to cancel the timeout, if needed */ + + if (token != null) + { + TimeoutService.cancelTimeoutHandler(token); + + /* Were we too late? */ + + synchronized (state) + { + if (state.timeoutSocketClosed) + throw new IOException("This exception will be replaced by the one below =)"); + /* + * Just in case the "cancelTimeoutHandler" invocation came + * just a little bit too late but the handler did not enter + * the semaphore yet - we can still stop it. + */ + state.isCancelled = true; + } + } + + return ci; + } + catch (SocketTimeoutException ste) + { + throw ste; + } + catch (IOException e1) + { + /* This will also invoke any registered connection monitors */ + close(new Throwable("There was a problem during connect."), false); + + synchronized (state) + { + /* + * Show a clean exception, not something like "the socket is + * closed!?!" + */ + if (state.timeoutSocketClosed) + throw new SocketTimeoutException("The kexTimeout (" + kexTimeout + " ms) expired."); + } + + /* Do not wrap a HTTPProxyException */ + if (e1 instanceof HTTPProxyException) + throw e1; + + throw (IOException) new IOException("There was a problem while connecting to " + hostname + ":" + port) + .initCause(e1); + } + } + + /** + * Creates a new {@link LocalPortForwarder}. A + * LocalPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host (which may or may + * not be identical to the remote SSH-2 server). + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param local_port + * the local port the LocalPortForwarder shall bind to. + * @param host_to_connect + * target address (IP or hostname) + * @param port_to_connect + * target port + * @return A {@link LocalPortForwarder} object. + * @throws IOException + */ + public synchronized LocalPortForwarder createLocalPortForwarder(int local_port, String host_to_connect, + int port_to_connect) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new LocalPortForwarder(cm, local_port, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link LocalPortForwarder}. A + * LocalPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host (which may or may + * not be identical to the remote SSH-2 server). + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param addr + * specifies the InetSocketAddress where the local socket shall + * be bound to. + * @param host_to_connect + * target address (IP or hostname) + * @param port_to_connect + * target port + * @return A {@link LocalPortForwarder} object. + * @throws IOException + */ + public synchronized LocalPortForwarder createLocalPortForwarder(InetSocketAddress addr, String host_to_connect, + int port_to_connect) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new LocalPortForwarder(cm, addr, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link LocalStreamForwarder}. A + * LocalStreamForwarder manages an Input/Outputstream pair + * that is being forwarded via the secure tunnel into a TCP/IP connection to + * another host (which may or may not be identical to the remote SSH-2 + * server). + * + * @param host_to_connect + * @param port_to_connect + * @return A {@link LocalStreamForwarder} object. + * @throws IOException + */ + public synchronized LocalStreamForwarder createLocalStreamForwarder(String host_to_connect, int port_to_connect) + throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward, connection is not authenticated."); + + return new LocalStreamForwarder(cm, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link DynamicPortForwarder}. A + * DynamicPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host that is chosen via + * the SOCKS protocol. + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param local_port + * @return A {@link DynamicPortForwarder} object. + * @throws IOException + */ + public synchronized DynamicPortForwarder createDynamicPortForwarder(int local_port) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new DynamicPortForwarder(cm, local_port); + } + + /** + * Creates a new {@link DynamicPortForwarder}. A + * DynamicPortForwarder forwards TCP/IP connections that arrive + * at a local port via the secure tunnel to another host that is chosen via + * the SOCKS protocol. + *

+ * This method must only be called after one has passed successfully the + * authentication step. There is no limit on the number of concurrent + * forwardings. + * + * @param addr + * specifies the InetSocketAddress where the local socket shall + * be bound to. + * @return A {@link DynamicPortForwarder} object. + * @throws IOException + */ + public synchronized DynamicPortForwarder createDynamicPortForwarder(InetSocketAddress addr) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new DynamicPortForwarder(cm, addr); + } + + /** + * Create a very basic {@link SCPClient} that can be used to copy files + * from/to the SSH-2 server. + *

+ * Works only after one has passed successfully the authentication step. + * There is no limit on the number of concurrent SCP clients. + *

+ * Note: This factory method will probably disappear in the future. + * + * @return A {@link SCPClient} object. + * @throws IOException + */ + public synchronized SCPClient createSCPClient() throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot create SCP client, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot create SCP client, connection is not authenticated."); + + return new SCPClient(this); + } + + /** + * Force an asynchronous key re-exchange (the call does not block). The + * latest values set for MAC, Cipher and DH group exchange parameters will + * be used. If a key exchange is currently in progress, then this method has + * the only effect that the so far specified parameters will be used for the + * next (server driven) key exchange. + *

+ * Note: This implementation will never start a key exchange (other than the + * initial one) unless you or the SSH-2 server ask for it. + * + * @throws IOException + * In case of any failure behind the scenes. + */ + public synchronized void forceKeyExchange() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + tm.forceKeyExchange(cryptoWishList, dhgexpara); + } + + /** + * Returns the hostname that was passed to the constructor. + * + * @return the hostname + */ + public synchronized String getHostname() + { + return hostname; + } + + /** + * Returns the port that was passed to the constructor. + * + * @return the TCP port + */ + public synchronized int getPort() + { + return port; + } + + /** + * Returns a {@link ConnectionInfo} object containing the details of the + * connection. Can be called as soon as the connection has been established + * (successfully connected). + * + * @return A {@link ConnectionInfo} object. + * @throws IOException + * In case of any failure behind the scenes. + */ + public synchronized ConnectionInfo getConnectionInfo() throws IOException + { + if (tm == null) + throw new IllegalStateException( + "Cannot get details of connection, you need to establish a connection first."); + return tm.getConnectionInfo(1); + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * can be used to tell which authentication methods are supported by the + * server at a certain stage of the authentication process (for the given + * username). + *

+ * Note 1: the username will only be used if no authentication step was done + * so far (it will be used to ask the server for a list of possible + * authentication methods by sending the initial "none" request). Otherwise, + * this method ignores the user name and returns a cached method list (which + * is based on the information contained in the last negative server + * response). + *

+ * Note 2: the server may return method names that are not supported by this + * implementation. + *

+ * After a successful authentication, this method must not be called + * anymore. + * + * @param user + * A String holding the username. + * + * @return a (possibly emtpy) array holding authentication method names. + * @throws IOException + */ + public synchronized String[] getRemainingAuthMethods(String user) throws IOException + { + if (user == null) + throw new IllegalArgumentException("user argument may not be NULL!"); + + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + return am.getRemainingMethods(user); + } + + /** + * Determines if the authentication phase is complete. Can be called at any + * time. + * + * @return true if no further authentication steps are + * needed. + */ + public synchronized boolean isAuthenticationComplete() + { + return authenticated; + } + + /** + * Returns true if there was at least one failed authentication request and + * the last failed authentication request was marked with "partial success" + * by the server. This is only needed in the rare case of SSH-2 server + * setups that cannot be satisfied with a single successful authentication + * request (i.e., multiple authentication steps are needed.) + *

+ * If you are interested in the details, then have a look at RFC4252. + * + * @return if the there was a failed authentication step and the last one + * was marked as a "partial success". + */ + public synchronized boolean isAuthenticationPartialSuccess() + { + if (am == null) + return false; + + return am.getPartialSuccess(); + } + + /** + * Checks if a specified authentication method is available. This method is + * actually just a wrapper for {@link #getRemainingAuthMethods(String) + * getRemainingAuthMethods()}. + * + * @param user + * A String holding the username. + * @param method + * An authentication method name (e.g., "publickey", "password", + * "keyboard-interactive") as specified by the SSH-2 standard. + * @return if the specified authentication method is currently available. + * @throws IOException + */ + public synchronized boolean isAuthMethodAvailable(String user, String method) throws IOException + { + if (method == null) + throw new IllegalArgumentException("method argument may not be NULL!"); + + String methods[] = getRemainingAuthMethods(user); + + for (int i = 0; i < methods.length; i++) + { + if (methods[i].compareTo(method) == 0) + return true; + } + + return false; + } + + private final SecureRandom getOrCreateSecureRND() + { + if (generator == null) + generator = new SecureRandom(); + + return generator; + } + + /** + * Open a new {@link Session} on this connection. Works only after one has + * passed successfully the authentication step. There is no limit on the + * number of concurrent sessions. + * + * @return A {@link Session} object. + * @throws IOException + */ + public synchronized Session openSession() throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot open session, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot open session, connection is not authenticated."); + + return new Session(cm, getOrCreateSecureRND()); + } + + /** + * Send an SSH_MSG_IGNORE packet. This method will generate a random data + * attribute (length between 0 (invlusive) and 16 (exclusive) bytes, + * contents are random bytes). + *

+ * This method must only be called once the connection is established. + * + * @throws IOException + */ + public synchronized void sendIgnorePacket() throws IOException + { + SecureRandom rnd = getOrCreateSecureRND(); + + byte[] data = new byte[rnd.nextInt(16)]; + rnd.nextBytes(data); + + sendIgnorePacket(data); + } + + /** + * Send an SSH_MSG_IGNORE packet with the given data attribute. + *

+ * This method must only be called once the connection is established. + * + * @throws IOException + */ + public synchronized void sendIgnorePacket(byte[] data) throws IOException + { + if (data == null) + throw new IllegalArgumentException("data argument must not be null."); + + if (tm == null) + throw new IllegalStateException( + "Cannot send SSH_MSG_IGNORE packet, you need to establish a connection first."); + + PacketIgnore pi = new PacketIgnore(); + pi.setData(data); + + tm.sendMessage(pi.getPayload()); + } + + /** + * Removes duplicates from a String array, keeps only first occurence of + * each element. Does not destroy order of elements; can handle nulls. Uses + * a very efficient O(N^2) algorithm =) + * + * @param list + * a String array. + * @return a cleaned String array. + */ + private String[] removeDuplicates(String[] list) + { + if ((list == null) || (list.length < 2)) + return list; + + String[] list2 = new String[list.length]; + + int count = 0; + + for (int i = 0; i < list.length; i++) + { + boolean duplicate = false; + + String element = list[i]; + + for (int j = 0; j < count; j++) + { + if (((element == null) && (list2[j] == null)) || ((element != null) && (element.equals(list2[j])))) + { + duplicate = true; + break; + } + } + + if (duplicate) + continue; + + list2[count++] = list[i]; + } + + if (count == list2.length) + return list2; + + String[] tmp = new String[count]; + System.arraycopy(list2, 0, tmp, 0, count); + + return tmp; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param ciphers + */ + public synchronized void setClient2ServerCiphers(String[] ciphers) + { + if ((ciphers == null) || (ciphers.length == 0)) + throw new IllegalArgumentException(); + ciphers = removeDuplicates(ciphers); + BlockCipherFactory.checkCipherList(ciphers); + cryptoWishList.c2s_enc_algos = ciphers; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param macs + */ + public synchronized void setClient2ServerMACs(String[] macs) + { + if ((macs == null) || (macs.length == 0)) + throw new IllegalArgumentException(); + macs = removeDuplicates(macs); + MAC.checkMacList(macs); + cryptoWishList.c2s_mac_algos = macs; + } + + /** + * Sets the parameters for the diffie-hellman group exchange. Unless you + * know what you are doing, you will never need this. Default values are + * defined in the {@link DHGexParameters} class. + * + * @param dgp + * {@link DHGexParameters}, non null. + * + */ + public synchronized void setDHGexParameters(DHGexParameters dgp) + { + if (dgp == null) + throw new IllegalArgumentException(); + + dhgexpara = dgp; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param ciphers + */ + public synchronized void setServer2ClientCiphers(String[] ciphers) + { + if ((ciphers == null) || (ciphers.length == 0)) + throw new IllegalArgumentException(); + ciphers = removeDuplicates(ciphers); + BlockCipherFactory.checkCipherList(ciphers); + cryptoWishList.s2c_enc_algos = ciphers; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param macs + */ + public synchronized void setServer2ClientMACs(String[] macs) + { + if ((macs == null) || (macs.length == 0)) + throw new IllegalArgumentException(); + + macs = removeDuplicates(macs); + MAC.checkMacList(macs); + cryptoWishList.s2c_mac_algos = macs; + } + + /** + * Define the set of allowed server host key algorithms to be used for the + * following key exchange operations. + *

+ * Unless you know what you are doing, you will never need this. + * + * @param algos + * An array of allowed server host key algorithms. SSH-2 defines + * ssh-dss and ssh-rsa. The + * entries of the array must be ordered after preference, i.e., + * the entry at index 0 is the most preferred one. You must + * specify at least one entry. + */ + public synchronized void setServerHostKeyAlgorithms(String[] algos) + { + if ((algos == null) || (algos.length == 0)) + throw new IllegalArgumentException(); + + algos = removeDuplicates(algos); + KexManager.checkServerHostkeyAlgorithmsList(algos); + cryptoWishList.serverHostKeyAlgorithms = algos; + } + + /** + * Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm) on the + * underlying socket. + *

+ * Can be called at any time. If the connection has not yet been established + * then the passed value will be stored and set after the socket has been + * set up. The default value that will be used is false. + * + * @param enable + * the argument passed to the Socket.setTCPNoDelay() + * method. + * @throws IOException + */ + public synchronized void setTCPNoDelay(boolean enable) throws IOException + { + tcpNoDelay = enable; + + if (tm != null) + tm.setTcpNoDelay(enable); + } + + /** + * Used to tell the library that the connection shall be established through + * a proxy server. It only makes sense to call this method before calling + * the {@link #connect() connect()} method. + *

+ * At the moment, only HTTP proxies are supported. + *

+ * Note: This method can be called any number of times. The + * {@link #connect() connect()} method will use the value set in the last + * preceding invocation of this method. + * + * @see HTTPProxyData + * + * @param proxyData + * Connection information about the proxy. If null, + * then no proxy will be used (non surprisingly, this is also the + * default). + */ + public synchronized void setProxyData(ProxyData proxyData) + { + this.proxyData = proxyData; + } + + /** + * Request a remote port forwarding. If successful, then forwarded + * connections will be redirected to the given target address. You can + * cancle a requested remote port forwarding by calling + * {@link #cancelRemotePortForwarding(int) cancelRemotePortForwarding()}. + *

+ * A call of this method will block until the peer either agreed or + * disagreed to your request- + *

+ * Note 1: this method typically fails if you + *

    + *
  • pass a port number for which the used remote user has not enough + * permissions (i.e., port < 1024)
  • + *
  • or pass a port number that is already in use on the remote server
  • + *
  • or if remote port forwarding is disabled on the server.
  • + *
+ *

+ * Note 2: (from the openssh man page): By default, the listening socket on + * the server will be bound to the loopback interface only. This may be + * overriden by specifying a bind address. Specifying a remote bind address + * will only succeed if the server's GatewayPorts option is enabled + * (see sshd_config(5)). + * + * @param bindAddress + * address to bind to on the server: + *

    + *
  • "" means that connections are to be accepted on all + * protocol families supported by the SSH implementation
  • + *
  • "0.0.0.0" means to listen on all IPv4 addresses
  • + *
  • "::" means to listen on all IPv6 addresses
  • + *
  • "localhost" means to listen on all protocol families + * supported by the SSH implementation on loopback addresses + * only, [RFC3330] and RFC3513]
  • + *
  • "127.0.0.1" and "::1" indicate listening on the loopback + * interfaces for IPv4 and IPv6 respectively
  • + *
+ * @param bindPort + * port number to bind on the server (must be > 0) + * @param targetAddress + * the target address (IP or hostname) + * @param targetPort + * the target port + * @throws IOException + */ + public synchronized void requestRemotePortForwarding(String bindAddress, int bindPort, String targetAddress, + int targetPort) throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + if ((bindAddress == null) || (targetAddress == null) || (bindPort <= 0) || (targetPort <= 0)) + throw new IllegalArgumentException(); + + cm.requestGlobalForward(bindAddress, bindPort, targetAddress, targetPort); + } + + /** + * Cancel an earlier requested remote port forwarding. Currently active + * forwardings will not be affected (e.g., disrupted). Note that further + * connection forwarding requests may be received until this method has + * returned. + * + * @param bindPort + * the allocated port number on the server + * @throws IOException + * if the remote side refuses the cancel request or another low + * level error occurs (e.g., the underlying connection is + * closed) + */ + public synchronized void cancelRemotePortForwarding(int bindPort) throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + cm.requestCancelGlobalForward(bindPort); + } + + /** + * Provide your own instance of SecureRandom. Can be used, e.g., if you want + * to seed the used SecureRandom generator manually. + *

+ * The SecureRandom instance is used during key exchanges, public key + * authentication, x11 cookie generation and the like. + * + * @param rnd + * a SecureRandom instance + */ + public synchronized void setSecureRandom(SecureRandom rnd) + { + if (rnd == null) + throw new IllegalArgumentException(); + + this.generator = rnd; + } + + /** + * Enable/disable debug logging. Only do this when requested by Trilead + * support. + *

+ * For speed reasons, some static variables used to check whether debugging + * is enabled are not protected with locks. In other words, if you + * dynamicaly enable/disable debug logging, then some threads may still use + * the old setting. To be on the safe side, enable debugging before doing + * the connect() call. + * + * @param enable + * on/off + * @param logger + * a {@link DebugLogger DebugLogger} instance, null + * means logging using the simple logger which logs all messages + * to to stderr. Ignored if enabled is false + */ + public synchronized void enableDebugging(boolean enable, DebugLogger logger) + { + Logger.enabled = enable; + + if (enable == false) + { + Logger.logger = null; + } + else + { + if (logger == null) + { + logger = new DebugLogger() + { + + public void log(int level, String className, String message) + { + long now = System.currentTimeMillis(); + System.err.println(now + " : " + className + ": " + message); + } + }; + } + + Logger.logger = logger; + } + } + + /** + * This method can be used to perform end-to-end connection testing. It + * sends a 'ping' message to the server and waits for the 'pong' from the + * server. + *

+ * When this method throws an exception, then you can assume that the + * connection should be abandoned. + *

+ * Note: Works only after one has passed successfully the authentication + * step. + *

+ * Implementation details: this method sends a SSH_MSG_GLOBAL_REQUEST + * request ('trilead-ping') to the server and waits for the + * SSH_MSG_REQUEST_FAILURE reply packet from the server. + * + * @throws IOException + * in case of any problem + */ + public synchronized void ping() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + cm.requestGlobalTrileadPing(); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/ConnectionInfo.java b/app/src/main/java/com/trilead/ssh2/ConnectionInfo.java new file mode 100644 index 0000000..b0ddbcf --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/ConnectionInfo.java @@ -0,0 +1,53 @@ + +package com.trilead.ssh2; + +/** + * In most cases you probably do not need the information contained in here. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ConnectionInfo.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class ConnectionInfo +{ + /** + * The used key exchange (KEX) algorithm in the latest key exchange. + */ + public String keyExchangeAlgorithm; + + /** + * The currently used crypto algorithm for packets from to the client to the + * server. + */ + public String clientToServerCryptoAlgorithm; + /** + * The currently used crypto algorithm for packets from to the server to the + * client. + */ + public String serverToClientCryptoAlgorithm; + + /** + * The currently used MAC algorithm for packets from to the client to the + * server. + */ + public String clientToServerMACAlgorithm; + /** + * The currently used MAC algorithm for packets from to the server to the + * client. + */ + public String serverToClientMACAlgorithm; + + /** + * The type of the server host key (currently either "ssh-dss" or + * "ssh-rsa"). + */ + public String serverHostKeyAlgorithm; + /** + * The server host key that was sent during the latest key exchange. + */ + public byte[] serverHostKey; + + /** + * Number of kex exchanges performed on this connection so far. + */ + public int keyExchangeCounter = 0; +} diff --git a/app/src/main/java/com/trilead/ssh2/ConnectionMonitor.java b/app/src/main/java/com/trilead/ssh2/ConnectionMonitor.java new file mode 100644 index 0000000..2f96d25 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/ConnectionMonitor.java @@ -0,0 +1,34 @@ + +package com.trilead.ssh2; + +/** + * A ConnectionMonitor is used to get notified when the + * underlying socket of a connection is closed. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ConnectionMonitor.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface ConnectionMonitor +{ + /** + * This method is called after the connection's underlying + * socket has been closed. E.g., due to the {@link Connection#close()} request of the + * user, if the peer closed the connection, due to a fatal error during connect() + * (also if the socket cannot be established) or if a fatal error occured on + * an established connection. + *

+ * This is an experimental feature. + *

+ * You MUST NOT make any assumption about the thread that invokes this method. + *

+ * Please note: if the connection is not connected (e.g., there was no successful + * connect() call), then the invocation of {@link Connection#close()} will NOT trigger + * this method. + * + * @see Connection#addConnectionMonitor(ConnectionMonitor) + * + * @param reason Includes an indication why the socket was closed. + */ + public void connectionLost(Throwable reason); +} \ No newline at end of file diff --git a/app/src/main/java/com/trilead/ssh2/DHGexParameters.java b/app/src/main/java/com/trilead/ssh2/DHGexParameters.java new file mode 100644 index 0000000..a2945ee --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/DHGexParameters.java @@ -0,0 +1,121 @@ + +package com.trilead.ssh2; + +/** + * A DHGexParameters object can be used to specify parameters for + * the diffie-hellman group exchange. + *

+ * Depending on which constructor is used, either the use of a + * SSH_MSG_KEX_DH_GEX_REQUEST or SSH_MSG_KEX_DH_GEX_REQUEST_OLD + * can be forced. + * + * @see Connection#setDHGexParameters(DHGexParameters) + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DHGexParameters.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class DHGexParameters +{ + private final int min_group_len; + private final int pref_group_len; + private final int max_group_len; + + private static final int MIN_ALLOWED = 1024; + private static final int MAX_ALLOWED = 8192; + + /** + * Same as calling {@link #DHGexParameters(int, int, int) DHGexParameters(1024, 1024, 4096)}. + * This is also the default used by the Connection class. + * + */ + public DHGexParameters() + { + this(1024, 1024, 4096); + } + + /** + * This constructor can be used to force the sending of a + * SSH_MSG_KEX_DH_GEX_REQUEST_OLD request. + * Internally, the minimum and maximum group lengths will + * be set to zero. + * + * @param pref_group_len has to be >= 1024 and <= 8192 + */ + public DHGexParameters(int pref_group_len) + { + if ((pref_group_len < MIN_ALLOWED) || (pref_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("pref_group_len out of range!"); + + this.pref_group_len = pref_group_len; + this.min_group_len = 0; + this.max_group_len = 0; + } + + /** + * This constructor can be used to force the sending of a + * SSH_MSG_KEX_DH_GEX_REQUEST request. + *

+ * Note: older OpenSSH servers don't understand this request, in which + * case you should use the {@link #DHGexParameters(int)} constructor. + *

+ * All values have to be >= 1024 and <= 8192. Furthermore, + * min_group_len <= pref_group_len <= max_group_len. + * + * @param min_group_len + * @param pref_group_len + * @param max_group_len + */ + public DHGexParameters(int min_group_len, int pref_group_len, int max_group_len) + { + if ((min_group_len < MIN_ALLOWED) || (min_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("min_group_len out of range!"); + + if ((pref_group_len < MIN_ALLOWED) || (pref_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("pref_group_len out of range!"); + + if ((max_group_len < MIN_ALLOWED) || (max_group_len > MAX_ALLOWED)) + throw new IllegalArgumentException("max_group_len out of range!"); + + if ((pref_group_len < min_group_len) || (pref_group_len > max_group_len)) + throw new IllegalArgumentException("pref_group_len is incompatible with min and max!"); + + if (max_group_len < min_group_len) + throw new IllegalArgumentException("max_group_len must not be smaller than min_group_len!"); + + this.min_group_len = min_group_len; + this.pref_group_len = pref_group_len; + this.max_group_len = max_group_len; + } + + /** + * Get the maximum group length. + * + * @return the maximum group length, may be zero if + * SSH_MSG_KEX_DH_GEX_REQUEST_OLD should be requested + */ + public int getMax_group_len() + { + return max_group_len; + } + + /** + * Get the minimum group length. + * + * @return minimum group length, may be zero if + * SSH_MSG_KEX_DH_GEX_REQUEST_OLD should be requested + */ + public int getMin_group_len() + { + return min_group_len; + } + + /** + * Get the preferred group length. + * + * @return the preferred group length + */ + public int getPref_group_len() + { + return pref_group_len; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/DebugLogger.java b/app/src/main/java/com/trilead/ssh2/DebugLogger.java new file mode 100644 index 0000000..adf2c8e --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/DebugLogger.java @@ -0,0 +1,23 @@ +package com.trilead.ssh2; + +/** + * An interface which needs to be implemented if you + * want to capture debugging messages. + * + * @see Connection#enableDebugging(boolean, DebugLogger) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DebugLogger.java,v 1.1 2008/03/03 07:01:36 cplattne Exp $ + */ +public interface DebugLogger +{ + +/** + * Log a debug message. + * + * @param level 0-99, 99 is a the most verbose level + * @param className the class that generated the message + * @param message the debug message + */ + public void log(int level, String className, String message); +} diff --git a/app/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java b/app/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java new file mode 100644 index 0000000..a15efe7 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java @@ -0,0 +1,67 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.DynamicAcceptThread; + +/** + * A DynamicPortForwarder forwards TCP/IP connections to a local + * port via the secure tunnel to another host which is selected via the + * SOCKS protocol. Checkout {@link Connection#createDynamicPortForwarder(int)} + * on how to create one. + * + * @author Kenny Root + * @version $Id: $ + */ +public class DynamicPortForwarder { + ChannelManager cm; + + DynamicAcceptThread dat; + + DynamicPortForwarder(ChannelManager cm, int local_port) + throws IOException + { + this.cm = cm; + + dat = new DynamicAcceptThread(cm, local_port); + dat.setDaemon(true); + dat.start(); + } + + DynamicPortForwarder(ChannelManager cm, InetSocketAddress addr) throws IOException { + this.cm = cm; + + dat = new DynamicAcceptThread(cm, addr); + dat.setDaemon(true); + dat.start(); + } + + /** + * Stop TCP/IP forwarding of newly arriving connections. + * + * @throws IOException + */ + public void close() throws IOException + { + dat.stopWorking(); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/HTTPProxyData.java b/app/src/main/java/com/trilead/ssh2/HTTPProxyData.java new file mode 100644 index 0000000..2edffe6 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/HTTPProxyData.java @@ -0,0 +1,83 @@ + +package com.trilead.ssh2; + +/** + * A HTTPProxyData object is used to specify the needed connection data + * to connect through a HTTP proxy. + * + * @see Connection#setProxyData(ProxyData) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: HTTPProxyData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class HTTPProxyData implements ProxyData +{ + public final String proxyHost; + public final int proxyPort; + public final String proxyUser; + public final String proxyPass; + public final String[] requestHeaderLines; + + /** + * Same as calling {@link #HTTPProxyData(String, int, String, String) HTTPProxyData(proxyHost, proxyPort, null, null)} + * + * @param proxyHost Proxy hostname. + * @param proxyPort Proxy port. + */ + public HTTPProxyData(String proxyHost, int proxyPort) + { + this(proxyHost, proxyPort, null, null); + } + + /** + * Same as calling {@link #HTTPProxyData(String, int, String, String, String[]) HTTPProxyData(proxyHost, proxyPort, null, null, null)} + * + * @param proxyHost Proxy hostname. + * @param proxyPort Proxy port. + * @param proxyUser Username for basic authentication (null if no authentication is needed). + * @param proxyPass Password for basic authentication (null if no authentication is needed). + */ + public HTTPProxyData(String proxyHost, int proxyPort, String proxyUser, String proxyPass) + { + this(proxyHost, proxyPort, proxyUser, proxyPass, null); + } + + /** + * Connection data for a HTTP proxy. It is possible to specify a username and password + * if the proxy requires basic authentication. Also, additional request header lines can + * be specified (e.g., "User-Agent: CERN-LineMode/2.15 libwww/2.17b3"). + *

+ * Please note: if you want to use basic authentication, then both proxyUser + * and proxyPass must be non-null. + *

+ * Here is an example: + *

+ * + * new HTTPProxyData("192.168.1.1", "3128", "proxyuser", "secret", new String[] {"User-Agent: TrileadBasedClient/1.0", "X-My-Proxy-Option: something"}); + * + * + * @param proxyHost Proxy hostname. + * @param proxyPort Proxy port. + * @param proxyUser Username for basic authentication (null if no authentication is needed). + * @param proxyPass Password for basic authentication (null if no authentication is needed). + * @param requestHeaderLines An array with additional request header lines (without end-of-line markers) + * that have to be sent to the server. May be null. + */ + + public HTTPProxyData(String proxyHost, int proxyPort, String proxyUser, String proxyPass, + String[] requestHeaderLines) + { + if (proxyHost == null) + throw new IllegalArgumentException("proxyHost must be non-null"); + + if (proxyPort < 0) + throw new IllegalArgumentException("proxyPort must be non-negative"); + + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyUser = proxyUser; + this.proxyPass = proxyPass; + this.requestHeaderLines = requestHeaderLines; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/HTTPProxyException.java b/app/src/main/java/com/trilead/ssh2/HTTPProxyException.java new file mode 100644 index 0000000..4687dd1 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/HTTPProxyException.java @@ -0,0 +1,29 @@ + +package com.trilead.ssh2; + +import java.io.IOException; + +/** + * May be thrown upon connect() if a HTTP proxy is being used. + * + * @see Connection#connect() + * @see Connection#setProxyData(ProxyData) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: HTTPProxyException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class HTTPProxyException extends IOException +{ + private static final long serialVersionUID = 2241537397104426186L; + + public final String httpResponse; + public final int httpErrorCode; + + public HTTPProxyException(String httpResponse, int httpErrorCode) + { + super("HTTP Proxy Error (" + httpErrorCode + " " + httpResponse + ")"); + this.httpResponse = httpResponse; + this.httpErrorCode = httpErrorCode; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/InteractiveCallback.java b/app/src/main/java/com/trilead/ssh2/InteractiveCallback.java new file mode 100644 index 0000000..3b83417 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/InteractiveCallback.java @@ -0,0 +1,55 @@ + +package com.trilead.ssh2; + +/** + * An InteractiveCallback is used to respond to challenges sent + * by the server if authentication mode "keyboard-interactive" is selected. + * + * @see Connection#authenticateWithKeyboardInteractive(String, + * String[], InteractiveCallback) + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: InteractiveCallback.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface InteractiveCallback +{ + /** + * This callback interface is used during a "keyboard-interactive" + * authentication. Every time the server sends a set of challenges (however, + * most often just one challenge at a time), this callback function will be + * called to give your application a chance to talk to the user and to + * determine the response(s). + *

+ * Some copy-paste information from the standard: a command line interface + * (CLI) client SHOULD print the name and instruction (if non-empty), adding + * newlines. Then for each prompt in turn, the client SHOULD display the + * prompt and read the user input. The name and instruction fields MAY be + * empty strings, the client MUST be prepared to handle this correctly. The + * prompt field(s) MUST NOT be empty strings. + *

+ * Please refer to draft-ietf-secsh-auth-kbdinteract-XX.txt for the details. + *

+ * Note: clients SHOULD use control character filtering as discussed in + * RFC4251 to avoid attacks by including + * terminal control characters in the fields to be displayed. + * + * @param name + * the name String sent by the server. + * @param instruction + * the instruction String sent by the server. + * @param numPrompts + * number of prompts - may be zero (in this case, you should just + * return a String array of length zero). + * @param prompt + * an array (length numPrompts) of Strings + * @param echo + * an array (length numPrompts) of booleans. For + * each prompt, the corresponding echo field indicates whether or + * not the user input should be echoed as characters are typed. + * @return an array of reponses - the array size must match the parameter + * numPrompts. + */ + public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) + throws Exception; +} diff --git a/app/src/main/java/com/trilead/ssh2/KnownHosts.java b/app/src/main/java/com/trilead/ssh2/KnownHosts.java new file mode 100644 index 0000000..ec022ff --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/KnownHosts.java @@ -0,0 +1,844 @@ + +package com.trilead.ssh2; + +import java.io.BufferedReader; +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Vector; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + + +/** + * The KnownHosts class is a handy tool to verify received server hostkeys + * based on the information in known_hosts files (the ones used by OpenSSH). + *

+ * It offers basically an in-memory database for known_hosts entries, as well as some + * helper functions. Entries from a known_hosts file can be loaded at construction time. + * It is also possible to add more keys later (e.g., one can parse different + * known_hosts files). + *

+ * It is a thread safe implementation, therefore, you need only to instantiate one + * KnownHosts for your whole application. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KnownHosts.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class KnownHosts +{ + public static final int HOSTKEY_IS_OK = 0; + public static final int HOSTKEY_IS_NEW = 1; + public static final int HOSTKEY_HAS_CHANGED = 2; + + private class KnownHostsEntry + { + String[] patterns; + PublicKey key; + + KnownHostsEntry(String[] patterns, PublicKey key) + { + this.patterns = patterns; + this.key = key; + } + } + + private LinkedList publicKeys = new LinkedList(); + + public KnownHosts() + { + } + + public KnownHosts(char[] knownHostsData) throws IOException + { + initialize(knownHostsData); + } + + public KnownHosts(File knownHosts) throws IOException + { + initialize(knownHosts); + } + + /** + * Adds a single public key entry to the database. Note: this will NOT add the public key + * to any physical file (e.g., "~/.ssh/known_hosts") - use addHostkeyToFile() for that purpose. + * This method is designed to be used in a {@link ServerHostKeyVerifier}. + * + * @param hostnames a list of hostname patterns - at least one most be specified. Check out the + * OpenSSH sshd man page for a description of the pattern matching algorithm. + * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. + * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. + * @throws IOException + */ + public void addHostkey(String hostnames[], String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException + { + if (hostnames == null) + throw new IllegalArgumentException("hostnames may not be null"); + + if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + { + RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, rpk)); + } + } + else if ("ssh-dss".equals(serverHostKeyAlgorithm)) + { + DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey); + + synchronized (publicKeys) + { + publicKeys.add(new KnownHostsEntry(hostnames, dpk)); + } + } + else if (serverHostKeyAlgorithm.startsWith(ECDSASHA2Verify.ECDSA_SHA2_PREFIX)) + { + ECPublicKey epk = ECDSASHA2Verify.decodeSSHECDSAPublicKey(serverHostKey); + + synchronized (publicKeys) { + publicKeys.add(new KnownHostsEntry(hostnames, epk)); + } + } + else + throw new IOException("Unknwon host key type (" + serverHostKeyAlgorithm + ")"); + } + + /** + * Parses the given known_hosts data and adds entries to the database. + * + * @param knownHostsData + * @throws IOException + */ + public void addHostkeys(char[] knownHostsData) throws IOException + { + initialize(knownHostsData); + } + + /** + * Parses the given known_hosts file and adds entries to the database. + * + * @param knownHosts + * @throws IOException + */ + public void addHostkeys(File knownHosts) throws IOException + { + initialize(knownHosts); + } + + /** + * Generate the hashed representation of the given hostname. Useful for adding entries + * with hashed hostnames to a known_hosts file. (see -H option of OpenSSH key-gen). + * + * @param hostname + * @return the hashed representation, e.g., "|1|cDhrv7zwEUV3k71CEPHnhHZezhA=|Xo+2y6rUXo2OIWRAYhBOIijbJMA=" + */ + public static final String createHashedHostname(String hostname) + { + MessageDigest sha1; + try { + sha1 = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("VM doesn't support SHA1", e); + } + + byte[] salt = new byte[sha1.getDigestLength()]; + + new SecureRandom().nextBytes(salt); + + byte[] hash = hmacSha1Hash(salt, hostname); + + String base64_salt = new String(Base64.encode(salt)); + String base64_hash = new String(Base64.encode(hash)); + + return new String("|1|" + base64_salt + "|" + base64_hash); + } + + private static final byte[] hmacSha1Hash(byte[] salt, String hostname) + { + Mac hmac; + try { + hmac = Mac.getInstance("HmacSHA1"); + if (salt.length != hmac.getMacLength()) + throw new IllegalArgumentException("Salt has wrong length (" + salt.length + ")"); + hmac.init(new SecretKeySpec(salt, "HmacSHA1")); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to HMAC-SHA1", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("Unable to create SecretKey", e); + } + + try + { + hmac.update(hostname.getBytes("ISO-8859-1")); + }catch(UnsupportedEncodingException ignore) + { + /* Actually, ISO-8859-1 is supported by all correct + * Java implementations. But... you never know. */ + hmac.update(hostname.getBytes()); + } + + return hmac.doFinal(); + } + + private final boolean checkHashed(String entry, String hostname) + { + if (entry.startsWith("|1|") == false) + return false; + + int delim_idx = entry.indexOf('|', 3); + + if (delim_idx == -1) + return false; + + String salt_base64 = entry.substring(3, delim_idx); + String hash_base64 = entry.substring(delim_idx + 1); + + byte[] salt = null; + byte[] hash = null; + + try + { + salt = Base64.decode(salt_base64.toCharArray()); + hash = Base64.decode(hash_base64.toCharArray()); + } + catch (IOException e) + { + return false; + } + + try { + MessageDigest sha1 = MessageDigest.getInstance("SHA1"); + if (salt.length != sha1.getDigestLength()) + return false; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("VM does not support SHA1", e); + } + + byte[] dig = hmacSha1Hash(salt, hostname); + + for (int i = 0; i < dig.length; i++) + if (dig[i] != hash[i]) + return false; + + return true; + } + + private int checkKey(String remoteHostname, PublicKey remoteKey) + { + int result = HOSTKEY_IS_NEW; + + synchronized (publicKeys) + { + Iterator i = publicKeys.iterator(); + + while (i.hasNext()) + { + KnownHostsEntry ke = i.next(); + + if (hostnameMatches(ke.patterns, remoteHostname) == false) + continue; + + boolean res = matchKeys(ke.key, remoteKey); + + if (res == true) + return HOSTKEY_IS_OK; + + result = HOSTKEY_HAS_CHANGED; + } + } + return result; + } + + private Vector getAllKeys(String hostname) + { + Vector keys = new Vector(); + + synchronized (publicKeys) + { + Iterator i = publicKeys.iterator(); + + while (i.hasNext()) + { + KnownHostsEntry ke = i.next(); + + if (hostnameMatches(ke.patterns, hostname) == false) + continue; + + keys.addElement(ke.key); + } + } + + return keys; + } + + /** + * Try to find the preferred order of hostkey algorithms for the given hostname. + * Based on the type of hostkey that is present in the internal database + * (i.e., either ssh-rsa or ssh-dss) + * an ordered list of hostkey algorithms is returned which can be passed + * to Connection.setServerHostKeyAlgorithms. + * + * @param hostname + * @return null if no key for the given hostname is present or + * there are keys of multiple types present for the given hostname. Otherwise, + * an array with hostkey algorithms is returned (i.e., an array of length 2). + */ + public String[] getPreferredServerHostkeyAlgorithmOrder(String hostname) + { + String[] algos = recommendHostkeyAlgorithms(hostname); + + if (algos != null) + return algos; + + InetAddress[] ipAdresses = null; + + try + { + ipAdresses = InetAddress.getAllByName(hostname); + } + catch (UnknownHostException e) + { + return null; + } + + for (int i = 0; i < ipAdresses.length; i++) + { + algos = recommendHostkeyAlgorithms(ipAdresses[i].getHostAddress()); + + if (algos != null) + return algos; + } + + return null; + } + + private final boolean hostnameMatches(String[] hostpatterns, String hostname) + { + boolean isMatch = false; + boolean negate = false; + + hostname = hostname.toLowerCase(Locale.US); + + for (int k = 0; k < hostpatterns.length; k++) + { + if (hostpatterns[k] == null) + continue; + + String pattern = null; + + /* In contrast to OpenSSH we also allow negated hash entries (as well as hashed + * entries in lines with multiple entries). + */ + + if ((hostpatterns[k].length() > 0) && (hostpatterns[k].charAt(0) == '!')) + { + pattern = hostpatterns[k].substring(1); + negate = true; + } + else + { + pattern = hostpatterns[k]; + negate = false; + } + + /* Optimize, no need to check this entry */ + + if ((isMatch) && (negate == false)) + continue; + + /* Now compare */ + + if (pattern.charAt(0) == '|') + { + if (checkHashed(pattern, hostname)) + { + if (negate) + return false; + isMatch = true; + } + } + else + { + pattern = pattern.toLowerCase(Locale.US); + + if ((pattern.indexOf('?') != -1) || (pattern.indexOf('*') != -1)) + { + if (pseudoRegex(pattern.toCharArray(), 0, hostname.toCharArray(), 0)) + { + if (negate) + return false; + isMatch = true; + } + } + else if (pattern.compareTo(hostname) == 0) + { + if (negate) + return false; + isMatch = true; + } + } + } + + return isMatch; + } + + private void initialize(char[] knownHostsData) throws IOException + { + BufferedReader br = new BufferedReader(new CharArrayReader(knownHostsData)); + + while (true) + { + String line = br.readLine(); + + if (line == null) + break; + + line = line.trim(); + + if (line.startsWith("#")) + continue; + + String[] arr = line.split(" "); + + if (arr.length >= 3) + { + if ((arr[1].compareTo("ssh-rsa") == 0) || (arr[1].compareTo("ssh-dss") == 0)) + { + String[] hostnames = arr[0].split(","); + + byte[] msg = Base64.decode(arr[2].toCharArray()); + + addHostkey(hostnames, arr[1], msg); + } + } + } + } + + private void initialize(File knownHosts) throws IOException + { + char[] buff = new char[512]; + + CharArrayWriter cw = new CharArrayWriter(); + + knownHosts.createNewFile(); + + FileReader fr = new FileReader(knownHosts); + + while (true) + { + int len = fr.read(buff); + if (len < 0) + break; + cw.write(buff, 0, len); + } + + fr.close(); + + initialize(cw.toCharArray()); + } + + private final boolean matchKeys(PublicKey key1, PublicKey key2) + { + return key1.equals(key2); + } + + private final boolean pseudoRegex(char[] pattern, int i, char[] match, int j) + { + /* This matching logic is equivalent to the one present in OpenSSH 4.1 */ + + while (true) + { + /* Are we at the end of the pattern? */ + + if (pattern.length == i) + return (match.length == j); + + if (pattern[i] == '*') + { + i++; + + if (pattern.length == i) + return true; + + if ((pattern[i] != '*') && (pattern[i] != '?')) + { + while (true) + { + if ((pattern[i] == match[j]) && pseudoRegex(pattern, i + 1, match, j + 1)) + return true; + j++; + if (match.length == j) + return false; + } + } + + while (true) + { + if (pseudoRegex(pattern, i, match, j)) + return true; + j++; + if (match.length == j) + return false; + } + } + + if (match.length == j) + return false; + + if ((pattern[i] != '?') && (pattern[i] != match[j])) + return false; + + i++; + j++; + } + } + + private String[] recommendHostkeyAlgorithms(String hostname) + { + String preferredAlgo = null; + + Vector keys = getAllKeys(hostname); + + for (int i = 0; i < keys.size(); i++) + { + String thisAlgo = null; + + if (keys.elementAt(i) instanceof RSAPublicKey) + thisAlgo = "ssh-rsa"; + else if (keys.elementAt(i) instanceof DSAPublicKey) + thisAlgo = "ssh-dss"; + else + continue; + + if (preferredAlgo != null) + { + /* If we find different key types, then return null */ + + if (preferredAlgo.compareTo(thisAlgo) != 0) + return null; + + /* OK, we found the same algo again, optimize */ + + continue; + } + } + + /* If we did not find anything that we know of, return null */ + + if (preferredAlgo == null) + return null; + + /* Now put the preferred algo to the start of the array. + * You may ask yourself why we do it that way - basically, we could just + * return only the preferred algorithm: since we have a saved key of that + * type (sent earlier from the remote host), then that should work out. + * However, imagine that the server is (for whatever reasons) not offering + * that type of hostkey anymore (e.g., "ssh-rsa" was disabled and + * now "ssh-dss" is being used). If we then do not let the server send us + * a fresh key of the new type, then we shoot ourself into the foot: + * the connection cannot be established and hence the user cannot decide + * if he/she wants to accept the new key. + */ + + if (preferredAlgo.equals("ssh-rsa")) + return new String[] { "ssh-rsa", "ssh-dss" }; + + return new String[] { "ssh-dss", "ssh-rsa" }; + } + + /** + * Checks the internal hostkey database for the given hostkey. + * If no matching key can be found, then the hostname is resolved to an IP address + * and the search is repeated using that IP address. + * + * @param hostname the server's hostname, will be matched with all hostname patterns + * @param serverHostKeyAlgorithm type of hostkey, either ssh-rsa or ssh-dss + * @param serverHostKey the key blob + * @return

    + *
  • HOSTKEY_IS_OK: the given hostkey matches an entry for the given hostname
  • + *
  • HOSTKEY_IS_NEW: no entries found for this hostname and this type of hostkey
  • + *
  • HOSTKEY_HAS_CHANGED: hostname is known, but with another key of the same type + * (man-in-the-middle attack?)
  • + *
+ * @throws IOException if the supplied key blob cannot be parsed or does not match the given hostkey type. + */ + public int verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException + { + PublicKey remoteKey = null; + + if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + { + remoteKey = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); + } + else if ("ssh-dss".equals(serverHostKeyAlgorithm)) + { + remoteKey = DSASHA1Verify.decodeSSHDSAPublicKey(serverHostKey); + } + else if (serverHostKeyAlgorithm.startsWith("ecdsa-sha2-")) + { + remoteKey = ECDSASHA2Verify.decodeSSHECDSAPublicKey(serverHostKey); + } + else + throw new IllegalArgumentException("Unknown hostkey type " + serverHostKeyAlgorithm); + + int result = checkKey(hostname, remoteKey); + + if (result == HOSTKEY_IS_OK) + return result; + + InetAddress[] ipAdresses = null; + + try + { + ipAdresses = InetAddress.getAllByName(hostname); + } + catch (UnknownHostException e) + { + return result; + } + + for (int i = 0; i < ipAdresses.length; i++) + { + int newresult = checkKey(ipAdresses[i].getHostAddress(), remoteKey); + + if (newresult == HOSTKEY_IS_OK) + return newresult; + + if (newresult == HOSTKEY_HAS_CHANGED) + result = HOSTKEY_HAS_CHANGED; + } + + return result; + } + + /** + * Adds a single public key entry to the a known_hosts file. + * This method is designed to be used in a {@link ServerHostKeyVerifier}. + * + * @param knownHosts the file where the publickey entry will be appended. + * @param hostnames a list of hostname patterns - at least one most be specified. Check out the + * OpenSSH sshd man page for a description of the pattern matching algorithm. + * @param serverHostKeyAlgorithm as passed to the {@link ServerHostKeyVerifier}. + * @param serverHostKey as passed to the {@link ServerHostKeyVerifier}. + * @throws IOException + */ + public final static void addHostkeyToFile(File knownHosts, String[] hostnames, String serverHostKeyAlgorithm, + byte[] serverHostKey) throws IOException + { + if ((hostnames == null) || (hostnames.length == 0)) + throw new IllegalArgumentException("Need at least one hostname specification"); + + if ((serverHostKeyAlgorithm == null) || (serverHostKey == null)) + throw new IllegalArgumentException(); + + CharArrayWriter writer = new CharArrayWriter(); + + for (int i = 0; i < hostnames.length; i++) + { + if (i != 0) + writer.write(','); + writer.write(hostnames[i]); + } + + writer.write(' '); + writer.write(serverHostKeyAlgorithm); + writer.write(' '); + writer.write(Base64.encode(serverHostKey)); + writer.write("\n"); + + char[] entry = writer.toCharArray(); + + RandomAccessFile raf = new RandomAccessFile(knownHosts, "rw"); + + long len = raf.length(); + + if (len > 0) + { + raf.seek(len - 1); + int last = raf.read(); + if (last != '\n') + raf.write('\n'); + } + + raf.write(new String(entry).getBytes("ISO-8859-1")); + raf.close(); + } + + /** + * Generates a "raw" fingerprint of a hostkey. + * + * @param type either "md5" or "sha1" + * @param keyType either "ssh-rsa" or "ssh-dss" + * @param hostkey the hostkey + * @return the raw fingerprint + */ + static final private byte[] rawFingerPrint(String type, String keyType, byte[] hostkey) + { + MessageDigest dig = null; + + try { + if ("md5".equals(type)) + { + dig = MessageDigest.getInstance("MD5"); + } + else if ("sha1".equals(type)) + { + dig = MessageDigest.getInstance("SHA1"); + } + else + { + throw new IllegalArgumentException("Unknown hash type " + type); + } + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unknown hash type " + type); + } + + if (keyType.startsWith("ecdsa-sha2-")) + { + } + else if ("ssh-rsa".equals(keyType)) + { + } + else if ("ssh-dss".equals(keyType)) + { + } + else + throw new IllegalArgumentException("Unknown key type " + keyType); + + if (hostkey == null) + throw new IllegalArgumentException("hostkey is null"); + + dig.update(hostkey); + return dig.digest(); + } + + /** + * Convert a raw fingerprint to hex representation (XX:YY:ZZ...). + * @param fingerprint raw fingerprint + * @return the hex representation + */ + static final private String rawToHexFingerprint(byte[] fingerprint) + { + final char[] alpha = "0123456789abcdef".toCharArray(); + + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < fingerprint.length; i++) + { + if (i != 0) + sb.append(':'); + int b = fingerprint[i] & 0xff; + sb.append(alpha[b >> 4]); + sb.append(alpha[b & 15]); + } + + return sb.toString(); + } + + /** + * Convert a raw fingerprint to bubblebabble representation. + * @param raw raw fingerprint + * @return the bubblebabble representation + */ + static final private String rawToBubblebabbleFingerprint(byte[] raw) + { + final char[] v = "aeiouy".toCharArray(); + final char[] c = "bcdfghklmnprstvzx".toCharArray(); + + StringBuffer sb = new StringBuffer(); + + int seed = 1; + + int rounds = (raw.length / 2) + 1; + + sb.append('x'); + + for (int i = 0; i < rounds; i++) + { + if (((i + 1) < rounds) || ((raw.length) % 2 != 0)) + { + sb.append(v[(((raw[2 * i] >> 6) & 3) + seed) % 6]); + sb.append(c[(raw[2 * i] >> 2) & 15]); + sb.append(v[((raw[2 * i] & 3) + (seed / 6)) % 6]); + + if ((i + 1) < rounds) + { + sb.append(c[(((raw[(2 * i) + 1])) >> 4) & 15]); + sb.append('-'); + sb.append(c[(((raw[(2 * i) + 1]))) & 15]); + // As long as seed >= 0, seed will be >= 0 afterwards + seed = ((seed * 5) + (((raw[2 * i] & 0xff) * 7) + (raw[(2 * i) + 1] & 0xff))) % 36; + } + } + else + { + sb.append(v[seed % 6]); // seed >= 0, therefore index positive + sb.append('x'); + sb.append(v[seed / 6]); + } + } + + sb.append('x'); + + return sb.toString(); + } + + /** + * Convert a ssh2 key-blob into a human readable hex fingerprint. + * Generated fingerprints are identical to those generated by OpenSSH. + *

+ * Example fingerprint: d0:cb:76:19:99:5a:03:fc:73:10:70:93:f2:44:63:47. + + * @param keytype either "ssh-rsa" or "ssh-dss" + * @param publickey key blob + * @return Hex fingerprint + */ + public final static String createHexFingerprint(String keytype, byte[] publickey) + { + byte[] raw = rawFingerPrint("md5", keytype, publickey); + return rawToHexFingerprint(raw); + } + + /** + * Convert a ssh2 key-blob into a human readable bubblebabble fingerprint. + * The used bubblebabble algorithm (taken from OpenSSH) generates fingerprints + * that are easier to remember for humans. + *

+ * Example fingerprint: xofoc-bubuz-cazin-zufyl-pivuk-biduk-tacib-pybur-gonar-hotat-lyxux. + * + * @param keytype either "ssh-rsa" or "ssh-dss" + * @param publickey key data + * @return Bubblebabble fingerprint + */ + public final static String createBubblebabbleFingerprint(String keytype, byte[] publickey) + { + byte[] raw = rawFingerPrint("sha1", keytype, publickey); + return rawToBubblebabbleFingerprint(raw); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/LocalPortForwarder.java b/app/src/main/java/com/trilead/ssh2/LocalPortForwarder.java new file mode 100644 index 0000000..96fa082 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/LocalPortForwarder.java @@ -0,0 +1,63 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.LocalAcceptThread; + + +/** + * A LocalPortForwarder forwards TCP/IP connections to a local + * port via the secure tunnel to another host (which may or may not be identical + * to the remote SSH-2 server). Checkout {@link Connection#createLocalPortForwarder(int, String, int)} + * on how to create one. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: LocalPortForwarder.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class LocalPortForwarder +{ + ChannelManager cm; + + String host_to_connect; + + int port_to_connect; + + LocalAcceptThread lat; + + LocalPortForwarder(ChannelManager cm, int local_port, String host_to_connect, int port_to_connect) + throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + lat = new LocalAcceptThread(cm, local_port, host_to_connect, port_to_connect); + lat.setDaemon(true); + lat.start(); + } + + LocalPortForwarder(ChannelManager cm, InetSocketAddress addr, String host_to_connect, int port_to_connect) + throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + lat = new LocalAcceptThread(cm, addr, host_to_connect, port_to_connect); + lat.setDaemon(true); + lat.start(); + } + + /** + * Stop TCP/IP forwarding of newly arriving connections. + * + * @throws IOException + */ + public void close() throws IOException + { + lat.stopWorking(); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java b/app/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java new file mode 100644 index 0000000..7899367 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java @@ -0,0 +1,78 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.trilead.ssh2.channel.Channel; +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.LocalAcceptThread; + + +/** + * A LocalStreamForwarder forwards an Input- and Outputstream + * pair via the secure tunnel to another host (which may or may not be identical + * to the remote SSH-2 server). + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: LocalStreamForwarder.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class LocalStreamForwarder +{ + ChannelManager cm; + + String host_to_connect; + int port_to_connect; + LocalAcceptThread lat; + + Channel cn; + + LocalStreamForwarder(ChannelManager cm, String host_to_connect, int port_to_connect) throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + cn = cm.openDirectTCPIPChannel(host_to_connect, port_to_connect, "127.0.0.1", 0); + } + + /** + * @return An InputStream object. + * @throws IOException + */ + public InputStream getInputStream() throws IOException + { + return cn.getStdoutStream(); + } + + /** + * Get the OutputStream. Please be aware that the implementation MAY use an + * internal buffer. To make sure that the buffered data is sent over the + * tunnel, you have to call the flush method of the + * OutputStream. To signal EOF, please use the + * close method of the OutputStream. + * + * @return An OutputStream object. + * @throws IOException + */ + public OutputStream getOutputStream() throws IOException + { + return cn.getStdinStream(); + } + + /** + * Close the underlying SSH forwarding channel and free up resources. + * You can also use this method to force the shutdown of the underlying + * forwarding channel. Pending output (OutputStream not flushed) will NOT + * be sent. Pending input (InputStream) can still be read. If the shutdown + * operation is already in progress (initiated from either side), then this + * call is a no-op. + * + * @throws IOException + */ + public void close() throws IOException + { + cm.closeChannel(cn, "Closed due to user request.", true); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/ProxyData.java b/app/src/main/java/com/trilead/ssh2/ProxyData.java new file mode 100644 index 0000000..059a6e3 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/ProxyData.java @@ -0,0 +1,15 @@ + +package com.trilead.ssh2; + +/** + * An abstract marker interface implemented by all proxy data implementations. + * + * @see HTTPProxyData + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ProxyData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public abstract interface ProxyData +{ +} diff --git a/app/src/main/java/com/trilead/ssh2/SCPClient.java b/app/src/main/java/com/trilead/ssh2/SCPClient.java new file mode 100644 index 0000000..b692750 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/SCPClient.java @@ -0,0 +1,729 @@ + +package com.trilead.ssh2; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A very basic SCPClient that can be used to copy files from/to + * the SSH-2 server. On the server side, the "scp" program must be in the PATH. + *

+ * This scp client is thread safe - you can download (and upload) different sets + * of files concurrently without any troubles. The SCPClient is + * actually mapping every request to a distinct {@link Session}. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SCPClient.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class SCPClient +{ + Connection conn; + + class LenNamePair + { + long length; + String filename; + } + + public SCPClient(Connection conn) + { + if (conn == null) + throw new IllegalArgumentException("Cannot accept null argument!"); + this.conn = conn; + } + + private void readResponse(InputStream is) throws IOException + { + int c = is.read(); + + if (c == 0) + return; + + if (c == -1) + throw new IOException("Remote scp terminated unexpectedly."); + + if ((c != 1) && (c != 2)) + throw new IOException("Remote scp sent illegal error code."); + + if (c == 2) + throw new IOException("Remote scp terminated with error."); + + String err = receiveLine(is); + throw new IOException("Remote scp terminated with error (" + err + ")."); + } + + private String receiveLine(InputStream is) throws IOException + { + StringBuffer sb = new StringBuffer(30); + + while (true) + { + /* + * This is a random limit - if your path names are longer, then + * adjust it + */ + + if (sb.length() > 8192) + throw new IOException("Remote scp sent a too long line"); + + int c = is.read(); + + if (c < 0) + throw new IOException("Remote scp terminated unexpectedly."); + + if (c == '\n') + break; + + sb.append((char) c); + + } + return sb.toString(); + } + + private LenNamePair parseCLine(String line) throws IOException + { + /* Minimum line: "xxxx y z" ---> 8 chars */ + + long len; + + if (line.length() < 8) + throw new IOException("Malformed C line sent by remote SCP binary, line too short."); + + if ((line.charAt(4) != ' ') || (line.charAt(5) == ' ')) + throw new IOException("Malformed C line sent by remote SCP binary."); + + int length_name_sep = line.indexOf(' ', 5); + + if (length_name_sep == -1) + throw new IOException("Malformed C line sent by remote SCP binary."); + + String length_substring = line.substring(5, length_name_sep); + String name_substring = line.substring(length_name_sep + 1); + + if ((length_substring.length() <= 0) || (name_substring.length() <= 0)) + throw new IOException("Malformed C line sent by remote SCP binary."); + + if ((6 + length_substring.length() + name_substring.length()) != line.length()) + throw new IOException("Malformed C line sent by remote SCP binary."); + + try + { + len = Long.parseLong(length_substring); + } + catch (NumberFormatException e) + { + throw new IOException("Malformed C line sent by remote SCP binary, cannot parse file length."); + } + + if (len < 0) + throw new IOException("Malformed C line sent by remote SCP binary, illegal file length."); + + LenNamePair lnp = new LenNamePair(); + lnp.length = len; + lnp.filename = name_substring; + + return lnp; + } + + private void sendBytes(Session sess, byte[] data, String fileName, String mode) throws IOException + { + OutputStream os = sess.getStdin(); + InputStream is = new BufferedInputStream(sess.getStdout(), 512); + + readResponse(is); + + String cline = "C" + mode + " " + data.length + " " + fileName + "\n"; + + os.write(cline.getBytes("ISO-8859-1")); + os.flush(); + + readResponse(is); + + os.write(data, 0, data.length); + os.write(0); + os.flush(); + + readResponse(is); + + os.write("E\n".getBytes("ISO-8859-1")); + os.flush(); + } + + private void sendFiles(Session sess, String[] files, String[] remoteFiles, String mode) throws IOException + { + byte[] buffer = new byte[8192]; + + OutputStream os = new BufferedOutputStream(sess.getStdin(), 40000); + InputStream is = new BufferedInputStream(sess.getStdout(), 512); + + readResponse(is); + + for (int i = 0; i < files.length; i++) + { + File f = new File(files[i]); + long remain = f.length(); + + String remoteName; + + if ((remoteFiles != null) && (remoteFiles.length > i) && (remoteFiles[i] != null)) + remoteName = remoteFiles[i]; + else + remoteName = f.getName(); + + String cline = "C" + mode + " " + remain + " " + remoteName + "\n"; + + os.write(cline.getBytes("ISO-8859-1")); + os.flush(); + + readResponse(is); + + FileInputStream fis = null; + + try + { + fis = new FileInputStream(f); + + while (remain > 0) + { + int trans; + if (remain > buffer.length) + trans = buffer.length; + else + trans = (int) remain; + + if (fis.read(buffer, 0, trans) != trans) + throw new IOException("Cannot read enough from local file " + files[i]); + + os.write(buffer, 0, trans); + + remain -= trans; + } + } + finally + { + if (fis != null) + fis.close(); + } + + os.write(0); + os.flush(); + + readResponse(is); + } + + os.write("E\n".getBytes("ISO-8859-1")); + os.flush(); + } + + private void receiveFiles(Session sess, OutputStream[] targets) throws IOException + { + byte[] buffer = new byte[8192]; + + OutputStream os = new BufferedOutputStream(sess.getStdin(), 512); + InputStream is = new BufferedInputStream(sess.getStdout(), 40000); + + os.write(0x0); + os.flush(); + + for (int i = 0; i < targets.length; i++) + { + LenNamePair lnp = null; + + while (true) + { + int c = is.read(); + if (c < 0) + throw new IOException("Remote scp terminated unexpectedly."); + + String line = receiveLine(is); + + if (c == 'T') + { + /* Ignore modification times */ + + continue; + } + + if ((c == 1) || (c == 2)) + throw new IOException("Remote SCP error: " + line); + + if (c == 'C') + { + lnp = parseCLine(line); + break; + + } + throw new IOException("Remote SCP error: " + ((char) c) + line); + } + + os.write(0x0); + os.flush(); + + long remain = lnp.length; + + while (remain > 0) + { + int trans; + if (remain > buffer.length) + trans = buffer.length; + else + trans = (int) remain; + + int this_time_received = is.read(buffer, 0, trans); + + if (this_time_received < 0) + { + throw new IOException("Remote scp terminated connection unexpectedly"); + } + + targets[i].write(buffer, 0, this_time_received); + + remain -= this_time_received; + } + + readResponse(is); + + os.write(0x0); + os.flush(); + } + } + + private void receiveFiles(Session sess, String[] files, String target) throws IOException + { + byte[] buffer = new byte[8192]; + + OutputStream os = new BufferedOutputStream(sess.getStdin(), 512); + InputStream is = new BufferedInputStream(sess.getStdout(), 40000); + + os.write(0x0); + os.flush(); + + for (int i = 0; i < files.length; i++) + { + LenNamePair lnp = null; + + while (true) + { + int c = is.read(); + if (c < 0) + throw new IOException("Remote scp terminated unexpectedly."); + + String line = receiveLine(is); + + if (c == 'T') + { + /* Ignore modification times */ + + continue; + } + + if ((c == 1) || (c == 2)) + throw new IOException("Remote SCP error: " + line); + + if (c == 'C') + { + lnp = parseCLine(line); + break; + + } + throw new IOException("Remote SCP error: " + ((char) c) + line); + } + + os.write(0x0); + os.flush(); + + File f = new File(target + File.separatorChar + lnp.filename); + FileOutputStream fop = null; + + try + { + fop = new FileOutputStream(f); + + long remain = lnp.length; + + while (remain > 0) + { + int trans; + if (remain > buffer.length) + trans = buffer.length; + else + trans = (int) remain; + + int this_time_received = is.read(buffer, 0, trans); + + if (this_time_received < 0) + { + throw new IOException("Remote scp terminated connection unexpectedly"); + } + + fop.write(buffer, 0, this_time_received); + + remain -= this_time_received; + } + } + finally + { + if (fop != null) + fop.close(); + } + + readResponse(is); + + os.write(0x0); + os.flush(); + } + } + + /** + * Copy a local file to a remote directory, uses mode 0600 when creating the + * file on the remote side. + * + * @param localFile + * Path and name of local file. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * + * @throws IOException + */ + public void put(String localFile, String remoteTargetDirectory) throws IOException + { + put(new String[] { localFile }, remoteTargetDirectory, "0600"); + } + + /** + * Copy a set of local files to a remote directory, uses mode 0600 when + * creating files on the remote side. + * + * @param localFiles + * Paths and names of local file names. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * + * @throws IOException + */ + + public void put(String[] localFiles, String remoteTargetDirectory) throws IOException + { + put(localFiles, remoteTargetDirectory, "0600"); + } + + /** + * Copy a local file to a remote directory, uses the specified mode when + * creating the file on the remote side. + * + * @param localFile + * Path and name of local file. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(String localFile, String remoteTargetDirectory, String mode) throws IOException + { + put(new String[] { localFile }, remoteTargetDirectory, mode); + } + + /** + * Copy a local file to a remote directory, uses the specified mode and + * remote filename when creating the file on the remote side. + * + * @param localFile + * Path and name of local file. + * @param remoteFileName + * The name of the file which will be created in the remote + * target directory. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(String localFile, String remoteFileName, String remoteTargetDirectory, String mode) + throws IOException + { + put(new String[] { localFile }, new String[] { remoteFileName }, remoteTargetDirectory, mode); + } + + /** + * Create a remote file and copy the contents of the passed byte array into + * it. Uses mode 0600 for creating the remote file. + * + * @param data + * the data to be copied into the remote file. + * @param remoteFileName + * The name of the file which will be created in the remote + * target directory. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @throws IOException + */ + + public void put(byte[] data, String remoteFileName, String remoteTargetDirectory) throws IOException + { + put(data, remoteFileName, remoteTargetDirectory, "0600"); + } + + /** + * Create a remote file and copy the contents of the passed byte array into + * it. The method use the specified mode when creating the file on the + * remote side. + * + * @param data + * the data to be copied into the remote file. + * @param remoteFileName + * The name of the file which will be created in the remote + * target directory. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(byte[] data, String remoteFileName, String remoteTargetDirectory, String mode) throws IOException + { + Session sess = null; + + if ((remoteFileName == null) || (remoteTargetDirectory == null) || (mode == null)) + throw new IllegalArgumentException("Null argument."); + + if (mode.length() != 4) + throw new IllegalArgumentException("Invalid mode."); + + for (int i = 0; i < mode.length(); i++) + if (Character.isDigit(mode.charAt(i)) == false) + throw new IllegalArgumentException("Invalid mode."); + + remoteTargetDirectory = remoteTargetDirectory.trim(); + remoteTargetDirectory = (remoteTargetDirectory.length() > 0) ? remoteTargetDirectory : "."; + + String cmd = "scp -t -d " + remoteTargetDirectory; + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + sendBytes(sess, data, remoteFileName, mode); + } + catch (IOException e) + { + throw (IOException) new IOException("Error during SCP transfer.").initCause(e); + } + finally + { + if (sess != null) + sess.close(); + } + } + + /** + * Copy a set of local files to a remote directory, uses the specified mode + * when creating the files on the remote side. + * + * @param localFiles + * Paths and names of the local files. + * @param remoteTargetDirectory + * Remote target directory. Use an empty string to specify the + * default directory. + * @param mode + * a four digit string (e.g., 0644, see "man chmod", "man open") + * @throws IOException + */ + public void put(String[] localFiles, String remoteTargetDirectory, String mode) throws IOException + { + put(localFiles, null, remoteTargetDirectory, mode); + } + + public void put(String[] localFiles, String[] remoteFiles, String remoteTargetDirectory, String mode) + throws IOException + { + Session sess = null; + + /* + * remoteFiles may be null, indicating that the local filenames shall be + * used + */ + + if ((localFiles == null) || (remoteTargetDirectory == null) || (mode == null)) + throw new IllegalArgumentException("Null argument."); + + if (mode.length() != 4) + throw new IllegalArgumentException("Invalid mode."); + + for (int i = 0; i < mode.length(); i++) + if (Character.isDigit(mode.charAt(i)) == false) + throw new IllegalArgumentException("Invalid mode."); + + if (localFiles.length == 0) + return; + + remoteTargetDirectory = remoteTargetDirectory.trim(); + remoteTargetDirectory = (remoteTargetDirectory.length() > 0) ? remoteTargetDirectory : "."; + + String cmd = "scp -t -d " + remoteTargetDirectory; + + for (int i = 0; i < localFiles.length; i++) + { + if (localFiles[i] == null) + throw new IllegalArgumentException("Cannot accept null filename."); + } + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + sendFiles(sess, localFiles, remoteFiles, mode); + } + catch (IOException e) + { + throw (IOException) new IOException("Error during SCP transfer.").initCause(e); + } + finally + { + if (sess != null) + sess.close(); + } + } + + /** + * Download a file from the remote server to a local directory. + * + * @param remoteFile + * Path and name of the remote file. + * @param localTargetDirectory + * Local directory to put the downloaded file. + * + * @throws IOException + */ + public void get(String remoteFile, String localTargetDirectory) throws IOException + { + get(new String[] { remoteFile }, localTargetDirectory); + } + + /** + * Download a file from the remote server and pipe its contents into an + * OutputStream. Please note that, to enable flexible usage + * of this method, the OutputStream will not be closed nor + * flushed. + * + * @param remoteFile + * Path and name of the remote file. + * @param target + * OutputStream where the contents of the file will be sent to. + * @throws IOException + */ + public void get(String remoteFile, OutputStream target) throws IOException + { + get(new String[] { remoteFile }, new OutputStream[] { target }); + } + + private void get(String remoteFiles[], OutputStream[] targets) throws IOException + { + Session sess = null; + + if ((remoteFiles == null) || (targets == null)) + throw new IllegalArgumentException("Null argument."); + + if (remoteFiles.length != targets.length) + throw new IllegalArgumentException("Length of arguments does not match."); + + if (remoteFiles.length == 0) + return; + + String cmd = "scp -f"; + + for (int i = 0; i < remoteFiles.length; i++) + { + if (remoteFiles[i] == null) + throw new IllegalArgumentException("Cannot accept null filename."); + + String tmp = remoteFiles[i].trim(); + + if (tmp.length() == 0) + throw new IllegalArgumentException("Cannot accept empty filename."); + + cmd += (" " + tmp); + } + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + receiveFiles(sess, targets); + } + catch (IOException e) + { + throw (IOException) new IOException("Error during SCP transfer.").initCause(e); + } + finally + { + if (sess != null) + sess.close(); + } + } + + /** + * Download a set of files from the remote server to a local directory. + * + * @param remoteFiles + * Paths and names of the remote files. + * @param localTargetDirectory + * Local directory to put the downloaded files. + * + * @throws IOException + */ + public void get(String remoteFiles[], String localTargetDirectory) throws IOException + { + Session sess = null; + + if ((remoteFiles == null) || (localTargetDirectory == null)) + throw new IllegalArgumentException("Null argument."); + + if (remoteFiles.length == 0) + return; + + String cmd = "scp -f"; + + for (int i = 0; i < remoteFiles.length; i++) + { + if (remoteFiles[i] == null) + throw new IllegalArgumentException("Cannot accept null filename."); + + String tmp = remoteFiles[i].trim(); + + if (tmp.length() == 0) + throw new IllegalArgumentException("Cannot accept empty filename."); + + cmd += (" " + tmp); + } + + try + { + sess = conn.openSession(); + sess.execCommand(cmd); + receiveFiles(sess, remoteFiles, localTargetDirectory); + } + catch (IOException e) + { + throw (IOException) new IOException("Error during SCP transfer.").initCause(e); + } + finally + { + if (sess != null) + sess.close(); + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/SFTPException.java b/app/src/main/java/com/trilead/ssh2/SFTPException.java new file mode 100644 index 0000000..d97723f --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/SFTPException.java @@ -0,0 +1,91 @@ + +package com.trilead.ssh2; + +import java.io.IOException; + +import com.trilead.ssh2.sftp.ErrorCodes; + + +/** + * Used in combination with the SFTPv3Client. This exception wraps + * error messages sent by the SFTP server. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class SFTPException extends IOException +{ + private static final long serialVersionUID = 578654644222421811L; + + private final String sftpErrorMessage; + private final int sftpErrorCode; + + private static String constructMessage(String s, int errorCode) + { + String[] detail = ErrorCodes.getDescription(errorCode); + + if (detail == null) + return s + " (UNKNOW SFTP ERROR CODE)"; + + return s + " (" + detail[0] + ": " + detail[1] + ")"; + } + + SFTPException(String msg, int errorCode) + { + super(constructMessage(msg, errorCode)); + sftpErrorMessage = msg; + sftpErrorCode = errorCode; + } + + /** + * Get the error message sent by the server. Often, this + * message does not help a lot (e.g., "failure"). + * + * @return the plain string as sent by the server. + */ + public String getServerErrorMessage() + { + return sftpErrorMessage; + } + + /** + * Get the error code sent by the server. + * + * @return an error code as defined in the SFTP specs. + */ + public int getServerErrorCode() + { + return sftpErrorCode; + } + + /** + * Get the symbolic name of the error code as given in the SFTP specs. + * + * @return e.g., "SSH_FX_INVALID_FILENAME". + */ + public String getServerErrorCodeSymbol() + { + String[] detail = ErrorCodes.getDescription(sftpErrorCode); + + if (detail == null) + return "UNKNOW SFTP ERROR CODE " + sftpErrorCode; + + return detail[0]; + } + + /** + * Get the description of the error code as given in the SFTP specs. + * + * @return e.g., "The filename is not valid." + */ + public String getServerErrorCodeVerbose() + { + String[] detail = ErrorCodes.getDescription(sftpErrorCode); + + if (detail == null) + return "The error code " + sftpErrorCode + " is unknown."; + + return detail[1]; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/SFTPv3Client.java b/app/src/main/java/com/trilead/ssh2/SFTPv3Client.java new file mode 100644 index 0000000..06796e9 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/SFTPv3Client.java @@ -0,0 +1,1388 @@ + +package com.trilead.ssh2; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Vector; + +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.sftp.AttribFlags; +import com.trilead.ssh2.sftp.ErrorCodes; +import com.trilead.ssh2.sftp.Packet; + + +/** + * A SFTPv3Client represents a SFTP (protocol version 3) + * client connection tunnelled over a SSH-2 connection. This is a very simple + * (synchronous) implementation. + *

+ * Basically, most methods in this class map directly to one of + * the packet types described in draft-ietf-secsh-filexfer-02.txt. + *

+ * Note: this is experimental code. + *

+ * Error handling: the methods of this class throw IOExceptions. However, unless + * there is catastrophic failure, exceptions of the type {@link SFTPv3Client} will + * be thrown (a subclass of IOException). Therefore, you can implement more verbose + * behavior by checking if a thrown exception if of this type. If yes, then you + * can cast the exception and access detailed information about the failure. + *

+ * Notes about file names, directory names and paths, copy-pasted + * from the specs: + *

    + *
  • SFTP v3 represents file names as strings. File names are + * assumed to use the slash ('/') character as a directory separator.
  • + *
  • File names starting with a slash are "absolute", and are relative to + * the root of the file system. Names starting with any other character + * are relative to the user's default directory (home directory).
  • + *
  • Servers SHOULD interpret a path name component ".." as referring to + * the parent directory, and "." as referring to the current directory. + * If the server implementation limits access to certain parts of the + * file system, it must be extra careful in parsing file names when + * enforcing such restrictions. There have been numerous reported + * security bugs where a ".." in a path name has allowed access outside + * the intended area.
  • + *
  • An empty path name is valid, and it refers to the user's default + * directory (usually the user's home directory).
  • + *
+ *

+ * If you are still not tired then please go on and read the comment for + * {@link #setCharset(String)}. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3Client.java,v 1.3 2008/04/01 12:38:09 cplattne Exp $ + */ +public class SFTPv3Client +{ + final Connection conn; + final Session sess; + final PrintStream debug; + + boolean flag_closed = false; + + InputStream is; + OutputStream os; + + int protocol_version = 0; + HashMap server_extensions = new HashMap(); + + int next_request_id = 1000; + + String charsetName = null; + + /** + * Create a SFTP v3 client. + * + * @param conn The underlying SSH-2 connection to be used. + * @param debug + * @throws IOException + * + * @deprecated this constructor (debug version) will disappear in the future, + * use {@link #SFTPv3Client(Connection)} instead. + */ + public SFTPv3Client(Connection conn, PrintStream debug) throws IOException + { + if (conn == null) + throw new IllegalArgumentException("Cannot accept null argument!"); + + this.conn = conn; + this.debug = debug; + + if (debug != null) + debug.println("Opening session and starting SFTP subsystem."); + + sess = conn.openSession(); + sess.startSubSystem("sftp"); + + is = sess.getStdout(); + os = new BufferedOutputStream(sess.getStdin(), 2048); + + if ((is == null) || (os == null)) + throw new IOException("There is a problem with the streams of the underlying channel."); + + init(); + } + + /** + * Create a SFTP v3 client. + * + * @param conn The underlying SSH-2 connection to be used. + * @throws IOException + */ + public SFTPv3Client(Connection conn) throws IOException + { + this(conn, null); + } + + /** + * Set the charset used to convert between Java Unicode Strings and byte encodings + * used by the server for paths and file names. Unfortunately, the SFTP v3 draft + * says NOTHING about such conversions (well, with the exception of error messages + * which have to be in UTF-8). Newer drafts specify to use UTF-8 for file names + * (if I remember correctly). However, a quick test using OpenSSH serving a EXT-3 + * filesystem has shown that UTF-8 seems to be a bad choice for SFTP v3 (tested with + * filenames containing german umlauts). "windows-1252" seems to work better for Europe. + * Luckily, "windows-1252" is the platform default in my case =). + *

+ * If you don't set anything, then the platform default will be used (this is the default + * behavior). + * + * @see #getCharset() + * + * @param charset the name of the charset to be used or null to use the platform's + * default encoding. + * @throws IOException + */ + public void setCharset(String charset) throws IOException + { + if (charset == null) + { + charsetName = charset; + return; + } + + try + { + Charset.forName(charset); + } + catch (Exception e) + { + throw (IOException) new IOException("This charset is not supported").initCause(e); + } + charsetName = charset; + } + + /** + * The currently used charset for filename encoding/decoding. + * + * @see #setCharset(String) + * + * @return The name of the charset (null if the platform's default charset is being used) + */ + public String getCharset() + { + return charsetName; + } + + private final void checkHandleValidAndOpen(SFTPv3FileHandle handle) throws IOException + { + if (handle.client != this) + throw new IOException("The file handle was created with another SFTPv3FileHandle instance."); + + if (handle.isClosed == true) + throw new IOException("The file handle is closed."); + } + + private final void sendMessage(int type, int requestId, byte[] msg, int off, int len) throws IOException + { + int msglen = len + 1; + + if (type != Packet.SSH_FXP_INIT) + msglen += 4; + + os.write(msglen >> 24); + os.write(msglen >> 16); + os.write(msglen >> 8); + os.write(msglen); + os.write(type); + + if (type != Packet.SSH_FXP_INIT) + { + os.write(requestId >> 24); + os.write(requestId >> 16); + os.write(requestId >> 8); + os.write(requestId); + } + + os.write(msg, off, len); + os.flush(); + } + + private final void sendMessage(int type, int requestId, byte[] msg) throws IOException + { + sendMessage(type, requestId, msg, 0, msg.length); + } + + private final void readBytes(byte[] buff, int pos, int len) throws IOException + { + while (len > 0) + { + int count = is.read(buff, pos, len); + if (count < 0) + throw new IOException("Unexpected end of sftp stream."); + if ((count == 0) || (count > len)) + throw new IOException("Underlying stream implementation is bogus!"); + len -= count; + pos += count; + } + } + + /** + * Read a message and guarantee that the contents is not larger than + * maxlen bytes. + *

+ * Note: receiveMessage(34000) actually means that the message may be up to 34004 + * bytes (the length attribute preceeding the contents is 4 bytes). + * + * @param maxlen + * @return the message contents + * @throws IOException + */ + private final byte[] receiveMessage(int maxlen) throws IOException + { + byte[] msglen = new byte[4]; + + readBytes(msglen, 0, 4); + + int len = (((msglen[0] & 0xff) << 24) | ((msglen[1] & 0xff) << 16) | ((msglen[2] & 0xff) << 8) | (msglen[3] & 0xff)); + + if ((len > maxlen) || (len <= 0)) + throw new IOException("Illegal sftp packet len: " + len); + + byte[] msg = new byte[len]; + + readBytes(msg, 0, len); + + return msg; + } + + private final int generateNextRequestID() + { + synchronized (this) + { + return next_request_id++; + } + } + + private final void closeHandle(byte[] handle) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle, 0, handle.length); + + sendMessage(Packet.SSH_FXP_CLOSE, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + private SFTPv3FileAttributes readAttrs(TypesReader tr) throws IOException + { + /* + * uint32 flags + * uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE + * uint32 uid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID + * uint32 gid present only if flag SSH_FILEXFER_ATTR_V3_UIDGID + * uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS + * uint32 atime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME + * uint32 mtime present only if flag SSH_FILEXFER_ATTR_V3_ACMODTIME + * uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED + * string extended_type + * string extended_data + * ... more extended data (extended_type - extended_data pairs), + * so that number of pairs equals extended_count + */ + + SFTPv3FileAttributes fa = new SFTPv3FileAttributes(); + + int flags = tr.readUINT32(); + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_SIZE) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_SIZE"); + fa.size = new Long(tr.readUINT64()); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_V3_UIDGID"); + fa.uid = new Integer(tr.readUINT32()); + fa.gid = new Integer(tr.readUINT32()); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_PERMISSIONS"); + fa.permissions = new Integer(tr.readUINT32()); + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME) != 0) + { + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_V3_ACMODTIME"); + fa.atime = new Long(((long)tr.readUINT32()) & 0xffffffffl); + fa.mtime = new Long(((long)tr.readUINT32()) & 0xffffffffl); + + } + + if ((flags & AttribFlags.SSH_FILEXFER_ATTR_EXTENDED) != 0) + { + int count = tr.readUINT32(); + + if (debug != null) + debug.println("SSH_FILEXFER_ATTR_EXTENDED (" + count + ")"); + + /* Read it anyway to detect corrupt packets */ + + while (count > 0) + { + tr.readByteString(); + tr.readByteString(); + count--; + } + } + + return fa; + } + + /** + * Retrieve the file attributes of an open file. + * + * @param handle a SFTPv3FileHandle handle. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes fstat(SFTPv3FileHandle handle) throws IOException + { + checkHandleValidAndOpen(handle); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + + if (debug != null) + { + debug.println("Sending SSH_FXP_FSTAT..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_FSTAT, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_ATTRS) + { + return readAttrs(tr); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + private SFTPv3FileAttributes statBoth(String path, int statMethod) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_STAT/SSH_FXP_LSTAT..."); + debug.flush(); + } + + sendMessage(statMethod, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_ATTRS) + { + return readAttrs(tr); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + /** + * Retrieve the file attributes of a file. This method + * follows symbolic links on the server. + * + * @see #lstat(String) + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes stat(String path) throws IOException + { + return statBoth(path, Packet.SSH_FXP_STAT); + } + + /** + * Retrieve the file attributes of a file. This method + * does NOT follow symbolic links on the server. + * + * @see #stat(String) + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileAttributes object. + * @throws IOException + */ + public SFTPv3FileAttributes lstat(String path) throws IOException + { + return statBoth(path, Packet.SSH_FXP_LSTAT); + } + + /** + * Read the target of a symbolic link. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return The target of the link. + * @throws IOException + */ + public String readLink(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_READLINK..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_READLINK, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (count != 1) + throw new IOException("The server sent an invalid SSH_FXP_NAME packet."); + + return tr.readString(charsetName); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + private void expectStatusOKMessage(int id) throws IOException + { + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != id) + throw new IOException("The server sent an invalid id field."); + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_OK) + return; + + throw new SFTPException(tr.readString(), errorCode); + } + + /** + * Modify the attributes of a file. Used for operations such as changing + * the ownership, permissions or access times, as well as for truncating a file. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be + * made to the attributes of the file. Empty fields will be ignored. + * @throws IOException + */ + public void setstat(String path, SFTPv3FileAttributes attr) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + tw.writeBytes(createAttrs(attr)); + + if (debug != null) + { + debug.println("Sending SSH_FXP_SETSTAT..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_SETSTAT, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Modify the attributes of a file. Used for operations such as changing + * the ownership, permissions or access times, as well as for truncating a file. + * + * @param handle a SFTPv3FileHandle handle + * @param attr A SFTPv3FileAttributes object. Specifies the modifications to be + * made to the attributes of the file. Empty fields will be ignored. + * @throws IOException + */ + public void fsetstat(SFTPv3FileHandle handle, SFTPv3FileAttributes attr) throws IOException + { + checkHandleValidAndOpen(handle); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeBytes(createAttrs(attr)); + + if (debug != null) + { + debug.println("Sending SSH_FXP_FSETSTAT..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_FSETSTAT, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Create a symbolic link on the server. Creates a link "src" that points + * to "target". + * + * @param src See the {@link SFTPv3Client comment} for the class for more details. + * @param target See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void createSymlink(String src, String target) throws IOException + { + int req_id = generateNextRequestID(); + + /* Either I am too stupid to understand the SFTP draft + * or the OpenSSH guys changed the semantics of src and target. + */ + + TypesWriter tw = new TypesWriter(); + tw.writeString(target, charsetName); + tw.writeString(src, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_SYMLINK..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_SYMLINK, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Have the server canonicalize any given path name to an absolute path. + * This is useful for converting path names containing ".." components or + * relative pathnames without a leading slash into absolute paths. + * + * @param path See the {@link SFTPv3Client comment} for the class for more details. + * @return An absolute path. + * @throws IOException + */ + public String canonicalPath(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_REALPATH..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_REALPATH, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (count != 1) + throw new IOException("The server sent an invalid SSH_FXP_NAME packet."); + + return tr.readString(charsetName); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + throw new SFTPException(tr.readString(), errorCode); + } + + private final Vector scanDirectory(byte[] handle) throws IOException + { + Vector files = new Vector(); + + while (true) + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle, 0, handle.length); + + if (debug != null) + { + debug.println("Sending SSH_FXP_READDIR..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_READDIR, req_id, tw.getBytes()); + + /* Some servers send here a packet with size > 34000 */ + /* To whom it may concern: please learn to read the specs. */ + + byte[] resp = receiveMessage(65536); + + if (debug != null) + { + debug.println("Got REPLY."); + debug.flush(); + } + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_NAME) + { + int count = tr.readUINT32(); + + if (debug != null) + debug.println("Parsing " + count + " name entries..."); + + while (count > 0) + { + SFTPv3DirectoryEntry dirEnt = new SFTPv3DirectoryEntry(); + + dirEnt.filename = tr.readString(charsetName); + dirEnt.longEntry = tr.readString(charsetName); + + dirEnt.attributes = readAttrs(tr); + files.addElement(dirEnt); + + if (debug != null) + debug.println("File: '" + dirEnt.filename + "'"); + count--; + } + continue; + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_EOF) + return files; + + throw new SFTPException(tr.readString(), errorCode); + } + } + + private final byte[] openDirectory(String path) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(path, charsetName); + + if (debug != null) + { + debug.println("Sending SSH_FXP_OPENDIR..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_OPENDIR, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_HANDLE) + { + if (debug != null) + { + debug.println("Got SSH_FXP_HANDLE."); + debug.flush(); + } + + byte[] handle = tr.readByteString(); + return handle; + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + + private final String expandString(byte[] b, int off, int len) + { + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < len; i++) + { + int c = b[off + i] & 0xff; + + if ((c >= 32) && (c <= 126)) + { + sb.append((char) c); + } + else + { + sb.append("{0x" + Integer.toHexString(c) + "}"); + } + } + + return sb.toString(); + } + + private void init() throws IOException + { + /* Send SSH_FXP_INIT (version 3) */ + + final int client_version = 3; + + if (debug != null) + debug.println("Sending SSH_FXP_INIT (" + client_version + ")..."); + + TypesWriter tw = new TypesWriter(); + tw.writeUINT32(client_version); + sendMessage(Packet.SSH_FXP_INIT, 0, tw.getBytes()); + + /* Receive SSH_FXP_VERSION */ + + if (debug != null) + debug.println("Waiting for SSH_FXP_VERSION..."); + + TypesReader tr = new TypesReader(receiveMessage(34000)); /* Should be enough for any reasonable server */ + + int type = tr.readByte(); + + if (type != Packet.SSH_FXP_VERSION) + { + throw new IOException("The server did not send a SSH_FXP_VERSION packet (got " + type + ")"); + } + + protocol_version = tr.readUINT32(); + + if (debug != null) + debug.println("SSH_FXP_VERSION: protocol_version = " + protocol_version); + + if (protocol_version != 3) + throw new IOException("Server version " + protocol_version + " is currently not supported"); + + /* Read and save extensions (if any) for later use */ + + while (tr.remain() != 0) + { + String name = tr.readString(); + byte[] value = tr.readByteString(); + server_extensions.put(name, value); + + if (debug != null) + debug.println("SSH_FXP_VERSION: extension: " + name + " = '" + expandString(value, 0, value.length) + + "'"); + } + } + + /** + * Returns the negotiated SFTP protocol version between the client and the server. + * + * @return SFTP protocol version, i.e., "3". + * + */ + public int getProtocolVersion() + { + return protocol_version; + } + + /** + * Close this SFTP session. NEVER forget to call this method to free up + * resources - even if you got an exception from one of the other methods. + * Sometimes these other methods may throw an exception, saying that the + * underlying channel is closed (this can happen, e.g., if the other server + * sent a close message.) However, as long as you have not called the + * close() method, you are likely wasting resources. + * + */ + public void close() + { + sess.close(); + } + + /** + * List the contents of a directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @return A Vector containing {@link SFTPv3DirectoryEntry} objects. + * @throws IOException + */ + public Vector ls(String dirName) throws IOException + { + byte[] handle = openDirectory(dirName); + Vector result = scanDirectory(handle); + closeHandle(handle); + return result; + } + + /** + * Create a new directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @param posixPermissions the permissions for this directory, e.g., "0700" (remember that + * this is octal noation). The server will likely apply a umask. + * + * @throws IOException + */ + public void mkdir(String dirName, int posixPermissions) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(dirName, charsetName); + tw.writeUINT32(AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS); + tw.writeUINT32(posixPermissions); + + sendMessage(Packet.SSH_FXP_MKDIR, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Remove a file. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void rm(String fileName) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(fileName, charsetName); + + sendMessage(Packet.SSH_FXP_REMOVE, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Remove an empty directory. + * + * @param dirName See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void rmdir(String dirName) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(dirName, charsetName); + + sendMessage(Packet.SSH_FXP_RMDIR, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Move a file or directory. + * + * @param oldPath See the {@link SFTPv3Client comment} for the class for more details. + * @param newPath See the {@link SFTPv3Client comment} for the class for more details. + * @throws IOException + */ + public void mv(String oldPath, String newPath) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(oldPath, charsetName); + tw.writeString(newPath, charsetName); + + sendMessage(Packet.SSH_FXP_RENAME, req_id, tw.getBytes()); + + expectStatusOKMessage(req_id); + } + + /** + * Open a file for reading. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRO(String fileName) throws IOException + { + return openFile(fileName, 0x00000001, null); // SSH_FXF_READ + } + + /** + * Open a file for reading and writing. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle openFileRW(String fileName) throws IOException + { + return openFile(fileName, 0x00000003, null); // SSH_FXF_READ | SSH_FXF_WRITE + } + + // Append is broken (already in the specification, because there is no way to + // send a write operation (what offset to use??)) + // public SFTPv3FileHandle openFileRWAppend(String fileName) throws IOException + // { + // return openFile(fileName, 0x00000007, null); // SSH_FXF_READ | SSH_FXF_WRITE | SSH_FXF_APPEND + // } + + /** + * Create a file and open it for reading and writing. + * Same as {@link #createFile(String, SFTPv3FileAttributes) createFile(fileName, null)}. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFile(String fileName) throws IOException + { + return createFile(fileName, null); + } + + /** + * Create a file and open it for reading and writing. + * You can specify the default attributes of the file (the server may or may + * not respect your wishes). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @param attr may be null to use server defaults. Probably only + * the uid, gid and permissions + * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle} + * structure make sense. You need only to set those fields where you want + * to override the server's defaults. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFile(String fileName, SFTPv3FileAttributes attr) throws IOException + { + return openFile(fileName, 0x00000008 | 0x00000003, attr); // SSH_FXF_CREAT | SSH_FXF_READ | SSH_FXF_WRITE + } + + /** + * Create a file (truncate it if it already exists) and open it for reading and writing. + * Same as {@link #createFileTruncate(String, SFTPv3FileAttributes) createFileTruncate(fileName, null)}. + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFileTruncate(String fileName) throws IOException + { + return createFileTruncate(fileName, null); + } + + /** + * reate a file (truncate it if it already exists) and open it for reading and writing. + * You can specify the default attributes of the file (the server may or may + * not respect your wishes). + * + * @param fileName See the {@link SFTPv3Client comment} for the class for more details. + * @param attr may be null to use server defaults. Probably only + * the uid, gid and permissions + * (remember the server may apply a umask) entries of the {@link SFTPv3FileHandle} + * structure make sense. You need only to set those fields where you want + * to override the server's defaults. + * @return a SFTPv3FileHandle handle + * @throws IOException + */ + public SFTPv3FileHandle createFileTruncate(String fileName, SFTPv3FileAttributes attr) throws IOException + { + return openFile(fileName, 0x00000018 | 0x00000003, attr); // SSH_FXF_CREAT | SSH_FXF_TRUNC | SSH_FXF_READ | SSH_FXF_WRITE + } + + private byte[] createAttrs(SFTPv3FileAttributes attr) + { + TypesWriter tw = new TypesWriter(); + + int attrFlags = 0; + + if (attr == null) + { + tw.writeUINT32(0); + } + else + { + if (attr.size != null) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_SIZE; + + if ((attr.uid != null) && (attr.gid != null)) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_UIDGID; + + if (attr.permissions != null) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_PERMISSIONS; + + if ((attr.atime != null) && (attr.mtime != null)) + attrFlags = attrFlags | AttribFlags.SSH_FILEXFER_ATTR_V3_ACMODTIME; + + tw.writeUINT32(attrFlags); + + if (attr.size != null) + tw.writeUINT64(attr.size.longValue()); + + if ((attr.uid != null) && (attr.gid != null)) + { + tw.writeUINT32(attr.uid.intValue()); + tw.writeUINT32(attr.gid.intValue()); + } + + if (attr.permissions != null) + tw.writeUINT32(attr.permissions.intValue()); + + if ((attr.atime != null) && (attr.mtime != null)) + { + tw.writeUINT32(attr.atime.intValue()); + tw.writeUINT32(attr.mtime.intValue()); + } + } + + return tw.getBytes(); + } + + private SFTPv3FileHandle openFile(String fileName, int flags, SFTPv3FileAttributes attr) throws IOException + { + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(fileName, charsetName); + tw.writeUINT32(flags); + tw.writeBytes(createAttrs(attr)); + + if (debug != null) + { + debug.println("Sending SSH_FXP_OPEN..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_OPEN, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_HANDLE) + { + if (debug != null) + { + debug.println("Got SSH_FXP_HANDLE."); + debug.flush(); + } + + return new SFTPv3FileHandle(this, tr.readByteString()); + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + + /** + * Read bytes from a file. No more than 32768 bytes may be read at once. + * Be aware that the semantics of read() are different than for Java streams. + *

+ *

    + *
  • The server will read as many bytes as it can from the file (up to len), + * and return them.
  • + *
  • If EOF is encountered before reading any data, -1 is returned. + *
  • If an error occurs, an exception is thrown
  • . + *
  • For normal disk files, it is guaranteed that the server will return the specified + * number of bytes, or up to end of file. For, e.g., device files this may return + * fewer bytes than requested.
  • + *
+ * + * @param handle a SFTPv3FileHandle handle + * @param fileOffset offset (in bytes) in the file + * @param dst the destination byte array + * @param dstoff offset in the destination byte array + * @param len how many bytes to read, 0 < len <= 32768 bytes + * @return the number of bytes that could be read, may be less than requested if + * the end of the file is reached, -1 is returned in case of EOF + * @throws IOException + */ + public int read(SFTPv3FileHandle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException + { + checkHandleValidAndOpen(handle); + + if ((len > 32768) || (len <= 0)) + throw new IllegalArgumentException("invalid len argument"); + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeUINT64(fileOffset); + tw.writeUINT32(len); + + if (debug != null) + { + debug.println("Sending SSH_FXP_READ..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_READ, req_id, tw.getBytes()); + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t == Packet.SSH_FXP_DATA) + { + if (debug != null) + { + debug.println("Got SSH_FXP_DATA..."); + debug.flush(); + } + + int readLen = tr.readUINT32(); + + if ((readLen < 0) || (readLen > len)) + throw new IOException("The server sent an invalid length field."); + + tr.readBytes(dst, dstoff, readLen); + + return readLen; + } + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_EOF) + { + if (debug != null) + { + debug.println("Got SSH_FX_EOF."); + debug.flush(); + } + + return -1; + } + + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + + /** + * Write bytes to a file. If len > 32768, then the write operation will + * be split into multiple writes. + * + * @param handle a SFTPv3FileHandle handle. + * @param fileOffset offset (in bytes) in the file. + * @param src the source byte array. + * @param srcoff offset in the source byte array. + * @param len how many bytes to write. + * @throws IOException + */ + public void write(SFTPv3FileHandle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException + { + checkHandleValidAndOpen(handle); + + while (len > 0) + { + int writeRequestLen = len; + + if (writeRequestLen > 32768) + writeRequestLen = 32768; + + int req_id = generateNextRequestID(); + + TypesWriter tw = new TypesWriter(); + tw.writeString(handle.fileHandle, 0, handle.fileHandle.length); + tw.writeUINT64(fileOffset); + tw.writeString(src, srcoff, writeRequestLen); + + if (debug != null) + { + debug.println("Sending SSH_FXP_WRITE..."); + debug.flush(); + } + + sendMessage(Packet.SSH_FXP_WRITE, req_id, tw.getBytes()); + + fileOffset += writeRequestLen; + + srcoff += writeRequestLen; + len -= writeRequestLen; + + byte[] resp = receiveMessage(34000); + + TypesReader tr = new TypesReader(resp); + + int t = tr.readByte(); + + int rep_id = tr.readUINT32(); + if (rep_id != req_id) + throw new IOException("The server sent an invalid id field."); + + if (t != Packet.SSH_FXP_STATUS) + throw new IOException("The SFTP server sent an unexpected packet type (" + t + ")"); + + int errorCode = tr.readUINT32(); + + if (errorCode == ErrorCodes.SSH_FX_OK) + continue; + + String errorMessage = tr.readString(); + + throw new SFTPException(errorMessage, errorCode); + } + } + + /** + * Close a file. + * + * @param handle a SFTPv3FileHandle handle + * @throws IOException + */ + public void closeFile(SFTPv3FileHandle handle) throws IOException + { + if (handle == null) + throw new IllegalArgumentException("the handle argument may not be null"); + + try + { + if (handle.isClosed == false) + { + closeHandle(handle.fileHandle); + } + } + finally + { + handle.isClosed = true; + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java b/app/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java new file mode 100644 index 0000000..669ba87 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java @@ -0,0 +1,38 @@ + +package com.trilead.ssh2; + +/** + * A SFTPv3DirectoryEntry as returned by {@link SFTPv3Client#ls(String)}. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3DirectoryEntry.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class SFTPv3DirectoryEntry +{ + /** + * A relative name within the directory, without any path components. + */ + public String filename; + + /** + * An expanded format for the file name, similar to what is returned by + * "ls -l" on Un*x systems. + *

+ * The format of this field is unspecified by the SFTP v3 protocol. + * It MUST be suitable for use in the output of a directory listing + * command (in fact, the recommended operation for a directory listing + * command is to simply display this data). However, clients SHOULD NOT + * attempt to parse the longname field for file attributes; they SHOULD + * use the attrs field instead. + *

+ * The recommended format for the longname field is as follows:
+ * -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer + */ + public String longEntry; + + /** + * The attributes of this entry. + */ + public SFTPv3FileAttributes attributes; +} diff --git a/app/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java b/app/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java new file mode 100644 index 0000000..7b1d321 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java @@ -0,0 +1,145 @@ + +package com.trilead.ssh2; + +/** + * A SFTPv3FileAttributes object represents detail information + * about a file on the server. Not all fields may/must be present. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3FileAttributes.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ + +public class SFTPv3FileAttributes +{ + /** + * The SIZE attribute. NULL if not present. + */ + public Long size = null; + + /** + * The UID attribute. NULL if not present. + */ + public Integer uid = null; + + /** + * The GID attribute. NULL if not present. + */ + public Integer gid = null; + + /** + * The POSIX permissions. NULL if not present. + *

+ * Here is a list: + *

+ *

Note: these numbers are all OCTAL.
+	 *  
+	 *  S_IFMT     0170000   bitmask for the file type bitfields
+	 *  S_IFSOCK   0140000   socket
+	 *  S_IFLNK    0120000   symbolic link
+	 *  S_IFREG    0100000   regular file
+	 *  S_IFBLK    0060000   block device
+	 *  S_IFDIR    0040000   directory
+	 *  S_IFCHR    0020000   character device
+	 *  S_IFIFO    0010000   fifo 
+	 *  S_ISUID    0004000   set UID bit
+	 *  S_ISGID    0002000   set GID bit 
+	 *  S_ISVTX    0001000   sticky bit
+	 *  
+	 *  S_IRWXU    00700     mask for file owner permissions
+	 *  S_IRUSR    00400     owner has read permission
+	 *  S_IWUSR    00200     owner has write permission
+	 *  S_IXUSR    00100     owner has execute permission
+	 *  S_IRWXG    00070     mask for group permissions
+	 *  S_IRGRP    00040     group has read permission
+	 *  S_IWGRP    00020     group has write permission
+	 *  S_IXGRP    00010     group has execute permission
+	 *  S_IRWXO    00007     mask for permissions for others (not in group)
+	 *  S_IROTH    00004     others have read permission
+	 *  S_IWOTH    00002     others have write permisson
+	 *  S_IXOTH    00001     others have execute permission
+	 * 
+ */ + public Integer permissions = null; + + /** + * The ATIME attribute. Represented as seconds from Jan 1, 1970 in UTC. + * NULL if not present. + */ + public Long atime = null; + + /** + * The MTIME attribute. Represented as seconds from Jan 1, 1970 in UTC. + * NULL if not present. + */ + public Long mtime = null; + + /** + * Checks if this entry is a directory. + * + * @return Returns true if permissions are available and they indicate + * that this entry represents a directory. + */ + public boolean isDirectory() + { + if (permissions == null) + return false; + + return ((permissions.intValue() & 0040000) != 0); + } + + /** + * Checks if this entry is a regular file. + * + * @return Returns true if permissions are available and they indicate + * that this entry represents a regular file. + */ + public boolean isRegularFile() + { + if (permissions == null) + return false; + + return ((permissions.intValue() & 0100000) != 0); + } + + /** + * Checks if this entry is a a symlink. + * + * @return Returns true if permissions are available and they indicate + * that this entry represents a symlink. + */ + public boolean isSymlink() + { + if (permissions == null) + return false; + + return ((permissions.intValue() & 0120000) != 0); + } + + /** + * Turn the POSIX permissions into a 7 digit octal representation. + * Note: the returned value is first masked with 0177777. + * + * @return NULL if permissions are not available. + */ + public String getOctalPermissions() + { + if (permissions == null) + return null; + + String res = Integer.toString(permissions.intValue() & 0177777, 8); + + StringBuffer sb = new StringBuffer(); + + int leadingZeros = 7 - res.length(); + + while (leadingZeros > 0) + { + sb.append('0'); + leadingZeros--; + } + + sb.append(res); + + return sb.toString(); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java b/app/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java new file mode 100644 index 0000000..9b3dbb6 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java @@ -0,0 +1,45 @@ + +package com.trilead.ssh2; + +/** + * A SFTPv3FileHandle. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SFTPv3FileHandle.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class SFTPv3FileHandle +{ + final SFTPv3Client client; + final byte[] fileHandle; + boolean isClosed = false; + + /* The constructor is NOT public */ + + SFTPv3FileHandle(SFTPv3Client client, byte[] h) + { + this.client = client; + this.fileHandle = h; + } + + /** + * Get the SFTPv3Client instance which created this handle. + * + * @return A SFTPv3Client instance. + */ + public SFTPv3Client getClient() + { + return client; + } + + /** + * Check if this handle was closed with the {@link SFTPv3Client#closeFile(SFTPv3FileHandle)} method + * of the SFTPv3Client instance which created the handle. + * + * @return if the handle is closed. + */ + public boolean isClosed() + { + return isClosed; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java b/app/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java new file mode 100644 index 0000000..ac65955 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java @@ -0,0 +1,31 @@ + +package com.trilead.ssh2; + +/** + * A callback interface used to implement a client specific method of checking + * server host keys. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ServerHostKeyVerifier.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public interface ServerHostKeyVerifier +{ + /** + * The actual verifier method, it will be called by the key exchange code + * on EVERY key exchange - this can happen several times during the lifetime + * of a connection. + *

+ * Note: SSH-2 servers are allowed to change their hostkey at ANY time. + * + * @param hostname the hostname used to create the {@link Connection} object + * @param port the remote TCP port + * @param serverHostKeyAlgorithm the public key algorithm (ssh-rsa or ssh-dss) + * @param serverHostKey the server's public key blob + * @return if the client wants to accept the server's host key - if not, the + * connection will be closed. + * @throws Exception Will be wrapped with an IOException, extended version of returning false =) + */ + public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) + throws Exception; +} diff --git a/app/src/main/java/com/trilead/ssh2/Session.java b/app/src/main/java/com/trilead/ssh2/Session.java new file mode 100644 index 0000000..bf3c7e0 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/Session.java @@ -0,0 +1,530 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; + +import com.trilead.ssh2.channel.Channel; +import com.trilead.ssh2.channel.ChannelManager; +import com.trilead.ssh2.channel.X11ServerData; + + +/** + * A Session is a remote execution of a program. "Program" means + * in this context either a shell, an application or a system command. The + * program may or may not have a tty. Only one single program can be started on + * a session. However, multiple sessions can be active simultaneously. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Session.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $ + */ +public class Session +{ + ChannelManager cm; + Channel cn; + + boolean flag_pty_requested = false; + boolean flag_x11_requested = false; + boolean flag_execution_started = false; + boolean flag_closed = false; + + String x11FakeCookie = null; + + final SecureRandom rnd; + + Session(ChannelManager cm, SecureRandom rnd) throws IOException + { + this.cm = cm; + this.cn = cm.openSessionChannel(); + this.rnd = rnd; + } + + /** + * Basically just a wrapper for lazy people - identical to calling + * requestPTY("dumb", 0, 0, 0, 0, null). + * + * @throws IOException + */ + public void requestDumbPTY() throws IOException + { + requestPTY("dumb", 0, 0, 0, 0, null); + } + + /** + * Basically just another wrapper for lazy people - identical to calling + * requestPTY(term, 0, 0, 0, 0, null). + * + * @throws IOException + */ + public void requestPTY(String term) throws IOException + { + requestPTY(term, 0, 0, 0, 0, null); + } + + /** + * Allocate a pseudo-terminal for this session. + *

+ * This method may only be called before a program or shell is started in + * this session. + *

+ * Different aspects can be specified: + *

+ *

    + *
  • The TERM environment variable value (e.g., vt100)
  • + *
  • The terminal's dimensions.
  • + *
  • The encoded terminal modes.
  • + *
+ * Zero dimension parameters are ignored. The character/row dimensions + * override the pixel dimensions (when nonzero). Pixel dimensions refer to + * the drawable area of the window. The dimension parameters are only + * informational. The encoding of terminal modes (parameter + * terminal_modes) is described in RFC4254. + * + * @param term + * The TERM environment variable value (e.g., vt100) + * @param term_width_characters + * terminal width, characters (e.g., 80) + * @param term_height_characters + * terminal height, rows (e.g., 24) + * @param term_width_pixels + * terminal width, pixels (e.g., 640) + * @param term_height_pixels + * terminal height, pixels (e.g., 480) + * @param terminal_modes + * encoded terminal modes (may be null) + * @throws IOException + */ + public void requestPTY(String term, int term_width_characters, int term_height_characters, int term_width_pixels, + int term_height_pixels, byte[] terminal_modes) throws IOException + { + if (term == null) + throw new IllegalArgumentException("TERM cannot be null."); + + if ((terminal_modes != null) && (terminal_modes.length > 0)) + { + if (terminal_modes[terminal_modes.length - 1] != 0) + throw new IOException("Illegal terminal modes description, does not end in zero byte"); + } + else + terminal_modes = new byte[] { 0 }; + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_pty_requested) + throw new IOException("A PTY was already requested."); + + if (flag_execution_started) + throw new IOException( + "Cannot request PTY at this stage anymore, a remote execution has already started."); + + flag_pty_requested = true; + } + + cm.requestPTY(cn, term, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels, + terminal_modes); + } + + /** + * Inform other side of connection that our PTY has resized. + *

+ * Zero dimension parameters are ignored. The character/row dimensions + * override the pixel dimensions (when nonzero). Pixel dimensions refer to + * the drawable area of the window. The dimension parameters are only + * informational. + * + * @param term_width_characters + * terminal width, characters (e.g., 80) + * @param term_height_characters + * terminal height, rows (e.g., 24) + * @param term_width_pixels + * terminal width, pixels (e.g., 640) + * @param term_height_pixels + * terminal height, pixels (e.g., 480) + * @throws IOException + */ + public void resizePTY(int term_width_characters, int term_height_characters, int term_width_pixels, + int term_height_pixels) throws IOException { + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + cm.resizePTY(cn, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels); + } + + /** + * Request X11 forwarding for the current session. + *

+ * You have to supply the name and port of your X-server. + *

+ * This method may only be called before a program or shell is started in + * this session. + * + * @param hostname the hostname of the real (target) X11 server (e.g., 127.0.0.1) + * @param port the port of the real (target) X11 server (e.g., 6010) + * @param cookie if non-null, then present this cookie to the real X11 server + * @param singleConnection if true, then the server is instructed to only forward one single + * connection, no more connections shall be forwarded after first, or after the session + * channel has been closed + * @throws IOException + */ + public void requestX11Forwarding(String hostname, int port, byte[] cookie, boolean singleConnection) + throws IOException + { + if (hostname == null) + throw new IllegalArgumentException("hostname argument may not be null"); + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_x11_requested) + throw new IOException("X11 forwarding was already requested."); + + if (flag_execution_started) + throw new IOException( + "Cannot request X11 forwarding at this stage anymore, a remote execution has already started."); + + flag_x11_requested = true; + } + + /* X11ServerData - used to store data about the target X11 server */ + + X11ServerData x11data = new X11ServerData(); + + x11data.hostname = hostname; + x11data.port = port; + x11data.x11_magic_cookie = cookie; /* if non-null, then present this cookie to the real X11 server */ + + /* Generate fake cookie - this one is used between remote clients and our proxy */ + + byte[] fakeCookie = new byte[16]; + String hexEncodedFakeCookie; + + /* Make sure that this fake cookie is unique for this connection */ + + while (true) + { + rnd.nextBytes(fakeCookie); + + /* Generate also hex representation of fake cookie */ + + StringBuffer tmp = new StringBuffer(32); + for (int i = 0; i < fakeCookie.length; i++) + { + String digit2 = Integer.toHexString(fakeCookie[i] & 0xff); + tmp.append((digit2.length() == 2) ? digit2 : "0" + digit2); + } + hexEncodedFakeCookie = tmp.toString(); + + /* Well, yes, chances are low, but we want to be on the safe side */ + + if (cm.checkX11Cookie(hexEncodedFakeCookie) == null) + break; + } + + /* Ask for X11 forwarding */ + + cm.requestX11(cn, singleConnection, "MIT-MAGIC-COOKIE-1", hexEncodedFakeCookie, 0); + + /* OK, that went fine, get ready to accept X11 connections... */ + /* ... but only if the user has not called close() in the meantime =) */ + + synchronized (this) + { + if (flag_closed == false) + { + this.x11FakeCookie = hexEncodedFakeCookie; + cm.registerX11Cookie(hexEncodedFakeCookie, x11data); + } + } + + /* Now it is safe to start remote X11 programs */ + } + + /** + * Execute a command on the remote machine. + * + * @param cmd + * The command to execute on the remote host. + * @throws IOException + */ + public void execCommand(String cmd) throws IOException + { + if (cmd == null) + throw new IllegalArgumentException("cmd argument may not be null"); + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_execution_started) + throw new IOException("A remote execution has already started."); + + flag_execution_started = true; + } + + cm.requestExecCommand(cn, cmd); + } + + /** + * Start a shell on the remote machine. + * + * @throws IOException + */ + public void startShell() throws IOException + { + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_execution_started) + throw new IOException("A remote execution has already started."); + + flag_execution_started = true; + } + + cm.requestShell(cn); + } + + /** + * Start a subsystem on the remote machine. + * Unless you know what you are doing, you will never need this. + * + * @param name the name of the subsystem. + * @throws IOException + */ + public void startSubSystem(String name) throws IOException + { + if (name == null) + throw new IllegalArgumentException("name argument may not be null"); + + synchronized (this) + { + /* The following is just a nicer error, we would catch it anyway later in the channel code */ + if (flag_closed) + throw new IOException("This session is closed."); + + if (flag_execution_started) + throw new IOException("A remote execution has already started."); + + flag_execution_started = true; + } + + cm.requestSubSystem(cn, name); + } + + /** + * This method can be used to perform end-to-end session (i.e., SSH channel) + * testing. It sends a 'ping' message to the server and waits for the 'pong' + * from the server. + *

+ * Implementation details: this method sends a SSH_MSG_CHANNEL_REQUEST request + * ('trilead-ping') to the server and waits for the SSH_MSG_CHANNEL_FAILURE reply + * packet. + * + * @throws IOException in case of any problem or when the session is closed + */ + public void ping() throws IOException + { + synchronized (this) + { + /* + * The following is just a nicer error, we would catch it anyway + * later in the channel code + */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + cm.requestChannelTrileadPing(cn); + } + + /** + * Request authentication agent forwarding. + * @param agent object that implements the callbacks + * + * @throws IOException in case of any problem or when the session is closed + */ + public synchronized boolean requestAuthAgentForwarding(AuthAgentCallback agent) throws IOException + { + synchronized (this) + { + /* + * The following is just a nicer error, we would catch it anyway + * later in the channel code + */ + if (flag_closed) + throw new IOException("This session is closed."); + } + + return cm.requestChannelAgentForwarding(cn, agent); + } + + public InputStream getStdout() + { + return cn.getStdoutStream(); + } + + public InputStream getStderr() + { + return cn.getStderrStream(); + } + + public OutputStream getStdin() + { + return cn.getStdinStream(); + } + + /** + * This method blocks until there is more data available on either the + * stdout or stderr InputStream of this Session. Very useful + * if you do not want to use two parallel threads for reading from the two + * InputStreams. One can also specify a timeout. NOTE: do NOT call this + * method if you use concurrent threads that operate on either of the two + * InputStreams of this Session (otherwise this method may + * block, even though more data is available). + * + * @param timeout + * The (non-negative) timeout in ms. 0 means no + * timeout, the call may block forever. + * @return + *

    + *
  • 0 if no more data will arrive.
  • + *
  • 1 if more data is available.
  • + *
  • -1 if a timeout occurred.
  • + *
+ * + * @throws IOException + * @deprecated This method has been replaced with a much more powerful wait-for-condition + * interface and therefore acts only as a wrapper. + * + */ + public int waitUntilDataAvailable(long timeout) throws IOException + { + if (timeout < 0) + throw new IllegalArgumentException("timeout must not be negative!"); + + int conditions = cm.waitForCondition(cn, timeout, ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA + | ChannelCondition.EOF); + + if ((conditions & ChannelCondition.TIMEOUT) != 0) + return -1; + + if ((conditions & (ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA)) != 0) + return 1; + + /* Here we do not need to check separately for CLOSED, since CLOSED implies EOF */ + + if ((conditions & ChannelCondition.EOF) != 0) + return 0; + + throw new IllegalStateException("Unexpected condition result (" + conditions + ")"); + } + + /** + * This method blocks until certain conditions hold true on the underlying SSH-2 channel. + *

+ * This method returns as soon as one of the following happens: + *

    + *
  • at least of the specified conditions (see {@link ChannelCondition}) holds true
  • + *
  • timeout > 0 and a timeout occured (TIMEOUT will be set in result conditions) + *
  • the underlying channel was closed (CLOSED will be set in result conditions) + *
+ *

+ * In any case, the result value contains ALL current conditions, which may be more + * than the specified condition set (i.e., never use the "==" operator to test for conditions + * in the bitmask, see also comments in {@link ChannelCondition}). + *

+ * Note: do NOT call this method if you want to wait for STDOUT_DATA or STDERR_DATA and + * there are concurrent threads (e.g., StreamGobblers) that operate on either of the two + * InputStreams of this Session (otherwise this method may + * block, even though more data is available in the StreamGobblers). + * + * @param condition_set a bitmask based on {@link ChannelCondition} values + * @param timeout non-negative timeout in ms, 0 means no timeout + * @return all bitmask specifying all current conditions that are true + */ + + public int waitForCondition(int condition_set, long timeout) + { + if (timeout < 0) + throw new IllegalArgumentException("timeout must be non-negative!"); + + return cm.waitForCondition(cn, timeout, condition_set); + } + + /** + * Get the exit code/status from the remote command - if available. Be + * careful - not all server implementations return this value. It is + * generally a good idea to call this method only when all data from the + * remote side has been consumed (see also the method). + * + * @return An Integer holding the exit code, or + * null if no exit code is (yet) available. + */ + public Integer getExitStatus() + { + return cn.getExitStatus(); + } + + /** + * Get the name of the signal by which the process on the remote side was + * stopped - if available and applicable. Be careful - not all server + * implementations return this value. + * + * @return An String holding the name of the signal, or + * null if the process exited normally or is still + * running (or if the server forgot to send this information). + */ + public String getExitSignal() + { + return cn.getExitSignal(); + } + + /** + * Close this session. NEVER forget to call this method to free up resources - + * even if you got an exception from one of the other methods (or when + * getting an Exception on the Input- or OutputStreams). Sometimes these other + * methods may throw an exception, saying that the underlying channel is + * closed (this can happen, e.g., if the other server sent a close message.) + * However, as long as you have not called the close() + * method, you may be wasting (local) resources. + * + */ + public void close() + { + synchronized (this) + { + if (flag_closed) + return; + + flag_closed = true; + + if (x11FakeCookie != null) + cm.unRegisterX11Cookie(x11FakeCookie, true); + + try + { + cm.closeChannel(cn, "Closed due to user request", true); + } + catch (IOException ignored) + { + } + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/StreamGobbler.java b/app/src/main/java/com/trilead/ssh2/StreamGobbler.java new file mode 100644 index 0000000..e93c388 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/StreamGobbler.java @@ -0,0 +1,229 @@ + +package com.trilead.ssh2; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A StreamGobbler is an InputStream that uses an internal worker + * thread to constantly consume input from another InputStream. It uses a buffer + * to store the consumed data. The buffer size is automatically adjusted, if needed. + *

+ * This class is sometimes very convenient - if you wrap a session's STDOUT and STDERR + * InputStreams with instances of this class, then you don't have to bother about + * the shared window of STDOUT and STDERR in the low level SSH-2 protocol, + * since all arriving data will be immediatelly consumed by the worker threads. + * Also, as a side effect, the streams will be buffered (e.g., single byte + * read() operations are faster). + *

+ * Other SSH for Java libraries include this functionality by default in + * their STDOUT and STDERR InputStream implementations, however, please be aware + * that this approach has also a downside: + *

+ * If you do not call the StreamGobbler's read() method often enough + * and the peer is constantly sending huge amounts of data, then you will sooner or later + * encounter a low memory situation due to the aggregated data (well, it also depends on the Java heap size). + * Joe Average will like this class anyway - a paranoid programmer would never use such an approach. + *

+ * The term "StreamGobbler" was taken from an article called "When Runtime.exec() won't", + * see http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: StreamGobbler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class StreamGobbler extends InputStream +{ + class GobblerThread extends Thread + { + public void run() + { + byte[] buff = new byte[8192]; + + while (true) + { + try + { + int avail = is.read(buff); + + synchronized (synchronizer) + { + if (avail <= 0) + { + isEOF = true; + synchronizer.notifyAll(); + break; + } + + int space_available = buffer.length - write_pos; + + if (space_available < avail) + { + /* compact/resize buffer */ + + int unread_size = write_pos - read_pos; + int need_space = unread_size + avail; + + byte[] new_buffer = buffer; + + if (need_space > buffer.length) + { + int inc = need_space / 3; + inc = (inc < 256) ? 256 : inc; + inc = (inc > 8192) ? 8192 : inc; + new_buffer = new byte[need_space + inc]; + } + + if (unread_size > 0) + System.arraycopy(buffer, read_pos, new_buffer, 0, unread_size); + + buffer = new_buffer; + + read_pos = 0; + write_pos = unread_size; + } + + System.arraycopy(buff, 0, buffer, write_pos, avail); + write_pos += avail; + + synchronizer.notifyAll(); + } + } + catch (IOException e) + { + synchronized (synchronizer) + { + exception = e; + synchronizer.notifyAll(); + break; + } + } + } + } + } + + private InputStream is; + private GobblerThread t; + + private Object synchronizer = new Object(); + + private boolean isEOF = false; + private boolean isClosed = false; + private IOException exception = null; + + private byte[] buffer = new byte[2048]; + private int read_pos = 0; + private int write_pos = 0; + + public StreamGobbler(InputStream is) + { + this.is = is; + t = new GobblerThread(); + t.setDaemon(true); + t.start(); + } + + public int read() throws IOException + { + synchronized (synchronizer) + { + if (isClosed) + throw new IOException("This StreamGobbler is closed."); + + while (read_pos == write_pos) + { + if (exception != null) + throw exception; + + if (isEOF) + return -1; + + try + { + synchronizer.wait(); + } + catch (InterruptedException e) + { + } + } + + int b = buffer[read_pos++] & 0xff; + + return b; + } + } + + public int available() throws IOException + { + synchronized (synchronizer) + { + if (isClosed) + throw new IOException("This StreamGobbler is closed."); + + return write_pos - read_pos; + } + } + + public int read(byte[] b) throws IOException + { + return read(b, 0, b.length); + } + + public void close() throws IOException + { + synchronized (synchronizer) + { + if (isClosed) + return; + isClosed = true; + isEOF = true; + synchronizer.notifyAll(); + is.close(); + } + } + + public int read(byte[] b, int off, int len) throws IOException + { + if (b == null) + throw new NullPointerException(); + + if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) + throw new IndexOutOfBoundsException(); + + if (len == 0) + return 0; + + synchronized (synchronizer) + { + if (isClosed) + throw new IOException("This StreamGobbler is closed."); + + while (read_pos == write_pos) + { + if (exception != null) + throw exception; + + if (isEOF) + return -1; + + try + { + synchronizer.wait(); + } + catch (InterruptedException e) + { + } + } + + int avail = write_pos - read_pos; + + avail = (avail > len) ? len : avail; + + System.arraycopy(buffer, read_pos, b, off, avail); + + read_pos += avail; + + return avail; + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java b/app/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java new file mode 100644 index 0000000..e551495 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java @@ -0,0 +1,466 @@ + +package com.trilead.ssh2.auth; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Vector; + +import com.trilead.ssh2.InteractiveCallback; +import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.packets.PacketServiceAccept; +import com.trilead.ssh2.packets.PacketServiceRequest; +import com.trilead.ssh2.packets.PacketUserauthBanner; +import com.trilead.ssh2.packets.PacketUserauthFailure; +import com.trilead.ssh2.packets.PacketUserauthInfoRequest; +import com.trilead.ssh2.packets.PacketUserauthInfoResponse; +import com.trilead.ssh2.packets.PacketUserauthRequestInteractive; +import com.trilead.ssh2.packets.PacketUserauthRequestNone; +import com.trilead.ssh2.packets.PacketUserauthRequestPassword; +import com.trilead.ssh2.packets.PacketUserauthRequestPublicKey; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; +import com.trilead.ssh2.transport.MessageHandler; +import com.trilead.ssh2.transport.TransportManager; + + +/** + * AuthenticationManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AuthenticationManager.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class AuthenticationManager implements MessageHandler +{ + TransportManager tm; + + Vector packets = new Vector(); + boolean connectionClosed = false; + + String banner; + + String[] remainingMethods = new String[0]; + boolean isPartialSuccess = false; + + boolean authenticated = false; + boolean initDone = false; + + public AuthenticationManager(TransportManager tm) + { + this.tm = tm; + } + + boolean methodPossible(String methName) + { + if (remainingMethods == null) + return false; + + for (int i = 0; i < remainingMethods.length; i++) + { + if (remainingMethods[i].compareTo(methName) == 0) + return true; + } + return false; + } + + byte[] deQueue() throws IOException + { + synchronized (packets) + { + while (packets.size() == 0) + { + if (connectionClosed) + throw (IOException) new IOException("The connection is closed.").initCause(tm + .getReasonClosedCause()); + + try + { + packets.wait(); + } + catch (InterruptedException ign) + { + } + } + /* This sequence works with J2ME */ + byte[] res = (byte[]) packets.firstElement(); + packets.removeElementAt(0); + return res; + } + } + + byte[] getNextMessage() throws IOException + { + while (true) + { + byte[] msg = deQueue(); + + if (msg[0] != Packets.SSH_MSG_USERAUTH_BANNER) + return msg; + + PacketUserauthBanner sb = new PacketUserauthBanner(msg, 0, msg.length); + + banner = sb.getBanner(); + } + } + + public String[] getRemainingMethods(String user) throws IOException + { + initialize(user); + return remainingMethods; + } + + public boolean getPartialSuccess() + { + return isPartialSuccess; + } + + private boolean initialize(String user) throws IOException + { + if (initDone == false) + { + tm.registerMessageHandler(this, 0, 255); + + PacketServiceRequest sr = new PacketServiceRequest("ssh-userauth"); + tm.sendMessage(sr.getPayload()); + + PacketUserauthRequestNone urn = new PacketUserauthRequestNone("ssh-connection", user); + tm.sendMessage(urn.getPayload()); + + byte[] msg = getNextMessage(); + new PacketServiceAccept(msg, 0, msg.length); + msg = getNextMessage(); + + initDone = true; + + if (msg[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (msg[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(msg, 0, msg.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + return false; + } + + throw new IOException("Unexpected SSH message (type " + msg[0] + ")"); + } + return authenticated; + } + + public boolean authenticatePublicKey(String user, char[] PEMPrivateKey, String password, SecureRandom rnd) + throws IOException + { + KeyPair pair = PEMDecoder.decode(PEMPrivateKey, password); + + return authenticatePublicKey(user, pair, rnd); + } + + public boolean authenticatePublicKey(String user, KeyPair pair, SecureRandom rnd) + throws IOException + { + PrivateKey key = pair.getPrivate(); + + try + { + initialize(user); + + if (methodPossible("publickey") == false) + throw new IOException("Authentication method publickey not supported by the server at this stage."); + + if (key instanceof DSAPrivateKey) + { + DSAPrivateKey pk = (DSAPrivateKey) key; + + byte[] pk_enc = DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pair.getPublic()); + + TypesWriter tw = new TypesWriter(); + + byte[] H = tm.getSessionIdentifier(); + + tw.writeString(H, 0, H.length); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(user); + tw.writeString("ssh-connection"); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString("ssh-dss"); + tw.writeString(pk_enc, 0, pk_enc.length); + + byte[] msg = tw.getBytes(); + + byte[] ds = DSASHA1Verify.generateSignature(msg, pk, rnd); + + byte[] ds_enc = DSASHA1Verify.encodeSSHDSASignature(ds); + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + "ssh-dss", pk_enc, ds_enc); + tm.sendMessage(ua.getPayload()); + } + else if (key instanceof RSAPrivateKey) + { + RSAPrivateKey pk = (RSAPrivateKey) key; + + byte[] pk_enc = RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pair.getPublic()); + + TypesWriter tw = new TypesWriter(); + { + byte[] H = tm.getSessionIdentifier(); + + tw.writeString(H, 0, H.length); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(user); + tw.writeString("ssh-connection"); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString("ssh-rsa"); + tw.writeString(pk_enc, 0, pk_enc.length); + } + + byte[] msg = tw.getBytes(); + + byte[] ds = RSASHA1Verify.generateSignature(msg, pk); + + byte[] rsa_sig_enc = RSASHA1Verify.encodeSSHRSASignature(ds); + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + "ssh-rsa", pk_enc, rsa_sig_enc); + + tm.sendMessage(ua.getPayload()); + } + else if (key instanceof ECPrivateKey) + { + ECPrivateKey pk = (ECPrivateKey) key; + final String algo = ECDSASHA2Verify.ECDSA_SHA2_PREFIX + + ECDSASHA2Verify.getCurveName(pk.getParams()); + + byte[] pk_enc = ECDSASHA2Verify.encodeSSHECDSAPublicKey((ECPublicKey) pair.getPublic()); + + TypesWriter tw = new TypesWriter(); + { + byte[] H = tm.getSessionIdentifier(); + + tw.writeString(H, 0, H.length); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(user); + tw.writeString("ssh-connection"); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString(algo); + tw.writeString(pk_enc, 0, pk_enc.length); + } + + byte[] msg = tw.getBytes(); + + byte[] ds = ECDSASHA2Verify.generateSignature(msg, pk); + + byte[] ec_sig_enc = ECDSASHA2Verify.encodeSSHECDSASignature(ds, pk.getParams()); + + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + algo, pk_enc, ec_sig_enc); + + tm.sendMessage(ua.getPayload()); + } + else + { + throw new IOException("Unknown private key type returned by the PEM decoder."); + } + + byte[] ar = getNextMessage(); + if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + + return false; + } + + throw new IOException("Unexpected SSH message (type " + ar[0] + ")"); + + } + catch (IOException e) + { +e.printStackTrace(); + tm.close(e, false); + throw (IOException) new IOException("Publickey authentication failed.").initCause(e); + } + } + + public boolean authenticateNone(String user) throws IOException + { + try + { + initialize(user); + return authenticated; + } + catch (IOException e) + { + tm.close(e, false); + throw (IOException) new IOException("None authentication failed.").initCause(e); + } + } + + public boolean authenticatePassword(String user, String pass) throws IOException + { + try + { + initialize(user); + + if (methodPossible("password") == false) + throw new IOException("Authentication method password not supported by the server at this stage."); + + PacketUserauthRequestPassword ua = new PacketUserauthRequestPassword("ssh-connection", user, pass); + tm.sendMessage(ua.getPayload()); + + byte[] ar = getNextMessage(); + + if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + + return false; + } + + throw new IOException("Unexpected SSH message (type " + ar[0] + ")"); + + } + catch (IOException e) + { + tm.close(e, false); + throw (IOException) new IOException("Password authentication failed.").initCause(e); + } + } + + public boolean authenticateInteractive(String user, String[] submethods, InteractiveCallback cb) throws IOException + { + try + { + initialize(user); + + if (methodPossible("keyboard-interactive") == false) + throw new IOException( + "Authentication method keyboard-interactive not supported by the server at this stage."); + + if (submethods == null) + submethods = new String[0]; + + PacketUserauthRequestInteractive ua = new PacketUserauthRequestInteractive("ssh-connection", user, + submethods); + + tm.sendMessage(ua.getPayload()); + + while (true) + { + byte[] ar = getNextMessage(); + + if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + + return false; + } + + if (ar[0] == Packets.SSH_MSG_USERAUTH_INFO_REQUEST) + { + PacketUserauthInfoRequest pui = new PacketUserauthInfoRequest(ar, 0, ar.length); + + String[] responses; + + try + { + responses = cb.replyToChallenge(pui.getName(), pui.getInstruction(), pui.getNumPrompts(), pui + .getPrompt(), pui.getEcho()); + } + catch (Exception e) + { + throw (IOException) new IOException("Exception in callback.").initCause(e); + } + + if (responses == null) + throw new IOException("Your callback may not return NULL!"); + + PacketUserauthInfoResponse puir = new PacketUserauthInfoResponse(responses); + tm.sendMessage(puir.getPayload()); + + continue; + } + + throw new IOException("Unexpected SSH message (type " + ar[0] + ")"); + } + } + catch (IOException e) + { + tm.close(e, false); + throw (IOException) new IOException("Keyboard-interactive authentication failed.").initCause(e); + } + } + + public void handleMessage(byte[] msg, int msglen) throws IOException + { + synchronized (packets) + { + if (msg == null) + { + connectionClosed = true; + } + else + { + byte[] tmp = new byte[msglen]; + System.arraycopy(msg, 0, tmp, 0, msglen); + packets.addElement(tmp); + } + + packets.notifyAll(); + + if (packets.size() > 5) + { + connectionClosed = true; + throw new IOException("Error, peer is flooding us with authentication packets."); + } + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java b/app/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java new file mode 100644 index 0000000..c6831e6 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java @@ -0,0 +1,579 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Map; +import java.util.Map.Entry; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + +/** + * AuthAgentForwardThread. + * + * @author Kenny Root + * @version $Id$ + */ +public class AuthAgentForwardThread extends Thread implements IChannelWorkerThread +{ + private static final byte[] SSH_AGENT_FAILURE = {0, 0, 0, 1, 5}; // 5 + private static final byte[] SSH_AGENT_SUCCESS = {0, 0, 0, 1, 6}; // 6 + + private static final int SSH2_AGENTC_REQUEST_IDENTITIES = 11; + private static final int SSH2_AGENT_IDENTITIES_ANSWER = 12; + + private static final int SSH2_AGENTC_SIGN_REQUEST = 13; + private static final int SSH2_AGENT_SIGN_RESPONSE = 14; + + private static final int SSH2_AGENTC_ADD_IDENTITY = 17; + private static final int SSH2_AGENTC_REMOVE_IDENTITY = 18; + private static final int SSH2_AGENTC_REMOVE_ALL_IDENTITIES = 19; + +// private static final int SSH_AGENTC_ADD_SMARTCARD_KEY = 20; +// private static final int SSH_AGENTC_REMOVE_SMARTCARD_KEY = 21; + + private static final int SSH_AGENTC_LOCK = 22; + private static final int SSH_AGENTC_UNLOCK = 23; + + private static final int SSH2_AGENTC_ADD_ID_CONSTRAINED = 25; +// private static final int SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED = 26; + + // Constraints for adding keys + private static final int SSH_AGENT_CONSTRAIN_LIFETIME = 1; + private static final int SSH_AGENT_CONSTRAIN_CONFIRM = 2; + + // Flags for signature requests +// private static final int SSH_AGENT_OLD_SIGNATURE = 1; + + private static final Logger log = Logger.getLogger(RemoteAcceptThread.class); + + AuthAgentCallback authAgent; + OutputStream os; + InputStream is; + Channel c; + + byte[] buffer = new byte[Channel.CHANNEL_BUFFER_SIZE]; + + public AuthAgentForwardThread(Channel c, AuthAgentCallback authAgent) + { + this.c = c; + this.authAgent = authAgent; + + if (log.isEnabled()) + log.log(20, "AuthAgentForwardThread started"); + } + + @Override + public void run() + { + try + { + c.cm.registerThread(this); + } + catch (IOException e) + { + stopWorking(); + return; + } + + try + { + c.cm.sendOpenConfirmation(c); + + is = c.getStdoutStream(); + os = c.getStdinStream(); + + int totalSize = 4; + int readSoFar = 0; + + while (true) { + int len; + + try + { + len = is.read(buffer, readSoFar, buffer.length - readSoFar); + } + catch (IOException e) + { + stopWorking(); + return; + } + + if (len <= 0) + break; + + readSoFar += len; + + if (readSoFar >= 4) { + TypesReader tr = new TypesReader(buffer, 0, 4); + totalSize = tr.readUINT32() + 4; + } + + if (totalSize == readSoFar) { + TypesReader tr = new TypesReader(buffer, 4, readSoFar - 4); + int messageType = tr.readByte(); + + switch (messageType) { + case SSH2_AGENTC_REQUEST_IDENTITIES: + sendIdentities(); + break; + case SSH2_AGENTC_ADD_IDENTITY: + addIdentity(tr, false); + break; + case SSH2_AGENTC_ADD_ID_CONSTRAINED: + addIdentity(tr, true); + break; + case SSH2_AGENTC_REMOVE_IDENTITY: + removeIdentity(tr); + break; + case SSH2_AGENTC_REMOVE_ALL_IDENTITIES: + removeAllIdentities(tr); + break; + case SSH2_AGENTC_SIGN_REQUEST: + processSignRequest(tr); + break; + case SSH_AGENTC_LOCK: + processLockRequest(tr); + break; + case SSH_AGENTC_UNLOCK: + processUnlockRequest(tr); + break; + default: + os.write(SSH_AGENT_FAILURE); + break; + } + + readSoFar = 0; + } + } + + c.cm.closeChannel(c, "EOF on both streams reached.", true); + } + catch (IOException e) + { + log.log(50, "IOException in agent forwarder: " + e.getMessage()); + + try + { + is.close(); + } + catch (IOException e1) + { + } + + try + { + os.close(); + } + catch (IOException e2) + { + } + + try + { + c.cm.closeChannel(c, "IOException in agent forwarder (" + e.getMessage() + ")", true); + } + catch (IOException e3) + { + } + } + } + + public void stopWorking() { + try + { + /* This will lead to an IOException in the is.read() call */ + is.close(); + } + catch (IOException e) + { + } + } + + /** + * @return whether the agent is locked + */ + private boolean failWhenLocked() throws IOException + { + if (authAgent.isAgentLocked()) { + os.write(SSH_AGENT_FAILURE); + return true; + } else + return false; + } + + private void sendIdentities() throws IOException + { + Map keys = null; + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_IDENTITIES_ANSWER); + int numKeys = 0; + + if (!authAgent.isAgentLocked()) + keys = authAgent.retrieveIdentities(); + + if (keys != null) + numKeys = keys.size(); + + tw.writeUINT32(numKeys); + + if (keys != null) { + for (Entry entry : keys.entrySet()) { + byte[] keyBytes = entry.getValue(); + tw.writeString(keyBytes, 0, keyBytes.length); + tw.writeString(entry.getKey()); + } + } + + sendPacket(tw.getBytes()); + } + + /** + * @param tr + */ + private void addIdentity(TypesReader tr, boolean checkConstraints) { + try + { + if (failWhenLocked()) + return; + + String type = tr.readString(); + + String comment; + String keyType; + KeySpec pubSpec; + KeySpec privSpec; + + if (type.equals("ssh-rsa")) { + keyType = "RSA"; + + BigInteger n = tr.readMPINT(); + BigInteger e = tr.readMPINT(); + BigInteger d = tr.readMPINT(); + BigInteger iqmp = tr.readMPINT(); + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + comment = tr.readString(); + + // Derive the extra values Java needs. + BigInteger dmp1 = d.mod(p.subtract(BigInteger.ONE)); + BigInteger dmq1 = d.mod(q.subtract(BigInteger.ONE)); + + pubSpec = new RSAPublicKeySpec(n, e); + privSpec = new RSAPrivateCrtKeySpec(n, e, d, p, q, dmp1, dmq1, iqmp); + } else if (type.equals("ssh-dss")) { + keyType = "DSA"; + + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + BigInteger g = tr.readMPINT(); + BigInteger y = tr.readMPINT(); + BigInteger x = tr.readMPINT(); + comment = tr.readString(); + + pubSpec = new DSAPublicKeySpec(y, p, q, g); + privSpec = new DSAPrivateKeySpec(x, p, q, g); + } else if (type.equals("ecdsa-sha2-nistp256")) { + keyType = "EC"; + + String curveName = tr.readString(); + byte[] groupBytes = tr.readByteString(); + BigInteger exponent = tr.readMPINT(); + comment = tr.readString(); + + if (!"nistp256".equals(curveName)) { + log.log(2, "Invalid curve name for ecdsa-sha2-nistp256: " + curveName); + os.write(SSH_AGENT_FAILURE); + return; + } + + ECParameterSpec nistp256 = ECDSASHA2Verify.EllipticCurves.nistp256; + ECPoint group = ECDSASHA2Verify.decodeECPoint(groupBytes, nistp256.getCurve()); + if (group == null) { + // TODO log error + os.write(SSH_AGENT_FAILURE); + return; + } + + pubSpec = new ECPublicKeySpec(group, nistp256); + privSpec = new ECPrivateKeySpec(exponent, nistp256); + } else { + log.log(2, "Unknown key type: " + type); + os.write(SSH_AGENT_FAILURE); + return; + } + + PublicKey pubKey; + PrivateKey privKey; + try { + KeyFactory kf = KeyFactory.getInstance(keyType); + pubKey = kf.generatePublic(pubSpec); + privKey = kf.generatePrivate(privSpec); + } catch (NoSuchAlgorithmException ex) { + // TODO: log error + os.write(SSH_AGENT_FAILURE); + return; + } catch (InvalidKeySpecException ex) { + // TODO: log error + os.write(SSH_AGENT_FAILURE); + return; + } + + KeyPair pair = new KeyPair(pubKey, privKey); + + boolean confirmUse = false; + int lifetime = 0; + + if (checkConstraints) { + while (tr.remain() > 0) { + int constraint = tr.readByte(); + if (constraint == SSH_AGENT_CONSTRAIN_CONFIRM) + confirmUse = true; + else if (constraint == SSH_AGENT_CONSTRAIN_LIFETIME) + lifetime = tr.readUINT32(); + else { + // Unknown constraint. Bail. + os.write(SSH_AGENT_FAILURE); + return; + } + } + } + + if (authAgent.addIdentity(pair, comment, confirmUse, lifetime)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeIdentity(TypesReader tr) { + try + { + if (failWhenLocked()) + return; + + byte[] publicKey = tr.readByteString(); + if (authAgent.removeIdentity(publicKey)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeAllIdentities(TypesReader tr) { + try + { + if (failWhenLocked()) + return; + + if (authAgent.removeAllIdentities()) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + private void processSignRequest(TypesReader tr) + { + try + { + if (failWhenLocked()) + return; + + byte[] publicKeyBytes = tr.readByteString(); + byte[] challenge = tr.readByteString(); + + int flags = tr.readUINT32(); + + if (flags != 0) { + // We don't understand any flags; abort! + os.write(SSH_AGENT_FAILURE); + return; + } + + KeyPair pair = authAgent.getKeyPair(publicKeyBytes); + + if (pair == null) { + os.write(SSH_AGENT_FAILURE); + return; + } + + byte[] response; + + PrivateKey privKey = pair.getPrivate(); + if (privKey instanceof RSAPrivateKey) { + byte[] signature = RSASHA1Verify.generateSignature(challenge, + (RSAPrivateKey) privKey); + response = RSASHA1Verify.encodeSSHRSASignature(signature); + } else if (privKey instanceof DSAPrivateKey) { + byte[] signature = DSASHA1Verify.generateSignature(challenge, + (DSAPrivateKey) privKey, new SecureRandom()); + response = DSASHA1Verify.encodeSSHDSASignature(signature); + } else { + os.write(SSH_AGENT_FAILURE); + return; + } + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_SIGN_RESPONSE); + tw.writeString(response, 0, response.length); + + sendPacket(tw.getBytes()); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void processLockRequest(TypesReader tr) { + try + { + if (failWhenLocked()) + return; + + String lockPassphrase = tr.readString(); + if (!authAgent.setAgentLock(lockPassphrase)) { + os.write(SSH_AGENT_FAILURE); + return; + } else + os.write(SSH_AGENT_SUCCESS); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void processUnlockRequest(TypesReader tr) + { + try + { + String unlockPassphrase = tr.readString(); + + if (authAgent.requestAgentUnlock(unlockPassphrase)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tw + * @throws IOException + */ + private void sendPacket(byte[] message) throws IOException + { + TypesWriter packet = new TypesWriter(); + packet.writeUINT32(message.length); + packet.writeBytes(message); + os.write(packet.getBytes()); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/Channel.java b/app/src/main/java/com/trilead/ssh2/channel/Channel.java new file mode 100644 index 0000000..8365f12 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/Channel.java @@ -0,0 +1,207 @@ + +package com.trilead.ssh2.channel; + +/** + * Channel. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Channel.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class Channel +{ + /* + * OK. Here is an important part of the JVM Specification: + * (http://java.sun.com/docs/books/vmspec/2nd-edition/html/Threads.doc.html#22214) + * + * Any association between locks and variables is purely conventional. + * Locking any lock conceptually flushes all variables from a thread's + * working memory, and unlocking any lock forces the writing out to main + * memory of all variables that the thread has assigned. That a lock may be + * associated with a particular object or a class is purely a convention. + * (...) + * + * If a thread uses a particular shared variable only after locking a + * particular lock and before the corresponding unlocking of that same lock, + * then the thread will read the shared value of that variable from main + * memory after the lock operation, if necessary, and will copy back to main + * memory the value most recently assigned to that variable before the + * unlock operation. + * + * This, in conjunction with the mutual exclusion rules for locks, suffices + * to guarantee that values are correctly transmitted from one thread to + * another through shared variables. + * + * ====> Always keep that in mind when modifying the Channel/ChannelManger + * code. + * + */ + + static final int STATE_OPENING = 1; + static final int STATE_OPEN = 2; + static final int STATE_CLOSED = 4; + + static final int CHANNEL_BUFFER_SIZE = 30000; + + /* + * To achieve correctness, the following rules have to be respected when + * accessing this object: + */ + + // These fields can always be read + final ChannelManager cm; + final ChannelOutputStream stdinStream; + final ChannelInputStream stdoutStream; + final ChannelInputStream stderrStream; + + // These two fields will only be written while the Channel is in state + // STATE_OPENING. + // The code makes sure that the two fields are written out when the state is + // changing to STATE_OPEN. + // Therefore, if you know that the Channel is in state STATE_OPEN, then you + // can read these two fields without synchronizing on the Channel. However, make + // sure that you get the latest values (e.g., flush caches by synchronizing on any + // object). However, to be on the safe side, you can lock the channel. + + int localID = -1; + int remoteID = -1; + + /* + * Make sure that we never send a data/EOF/WindowChange msg after a CLOSE + * msg. + * + * This is a little bit complicated, but we have to do it in that way, since + * we cannot keep a lock on the Channel during the send operation (this + * would block sometimes the receiver thread, and, in extreme cases, can + * lead to a deadlock on both sides of the connection (senders are blocked + * since the receive buffers on the other side are full, and receiver + * threads wait for the senders to finish). It all depends on the + * implementation on the other side. But we cannot make any assumptions, we + * have to assume the worst case. Confused? Just believe me. + */ + + /* + * If you send a message on a channel, then you have to aquire the + * "channelSendLock" and check the "closeMessageSent" flag (this variable + * may only be accessed while holding the "channelSendLock" !!! + * + * BTW: NEVER EVER SEND MESSAGES FROM THE RECEIVE THREAD - see explanation + * above. + */ + + final Object channelSendLock = new Object(); + boolean closeMessageSent = false; + + /* + * Stop memory fragmentation by allocating this often used buffer. + * May only be used while holding the channelSendLock + */ + + final byte[] msgWindowAdjust = new byte[9]; + + // If you access (read or write) any of the following fields, then you have + // to synchronize on the channel. + + int state = STATE_OPENING; + + boolean closeMessageRecv = false; + + /* This is a stupid implementation. At the moment we can only wait + * for one pending request per channel. + */ + int successCounter = 0; + int failedCounter = 0; + + int localWindow = 0; /* locally, we use a small window, < 2^31 */ + long remoteWindow = 0; /* long for readable 2^32 - 1 window support */ + + int localMaxPacketSize = -1; + int remoteMaxPacketSize = -1; + + final byte[] stdoutBuffer = new byte[CHANNEL_BUFFER_SIZE]; + final byte[] stderrBuffer = new byte[CHANNEL_BUFFER_SIZE]; + + int stdoutReadpos = 0; + int stdoutWritepos = 0; + int stderrReadpos = 0; + int stderrWritepos = 0; + + boolean EOF = false; + + Integer exit_status; + + String exit_signal; + + // we keep the x11 cookie so that this channel can be closed when this + // specific x11 forwarding gets stopped + + String hexX11FakeCookie; + + // reasonClosed is special, since we sometimes need to access it + // while holding the channelSendLock. + // We protect it with a private short term lock. + + private final Object reasonClosedLock = new Object(); + private String reasonClosed = null; + + public Channel(ChannelManager cm) + { + this.cm = cm; + + this.localWindow = CHANNEL_BUFFER_SIZE; + this.localMaxPacketSize = 35000 - 1024; // leave enough slack + + this.stdinStream = new ChannelOutputStream(this); + this.stdoutStream = new ChannelInputStream(this, false); + this.stderrStream = new ChannelInputStream(this, true); + } + + /* Methods to allow access from classes outside of this package */ + + public ChannelInputStream getStderrStream() + { + return stderrStream; + } + + public ChannelOutputStream getStdinStream() + { + return stdinStream; + } + + public ChannelInputStream getStdoutStream() + { + return stdoutStream; + } + + public String getExitSignal() + { + synchronized (this) + { + return exit_signal; + } + } + + public Integer getExitStatus() + { + synchronized (this) + { + return exit_status; + } + } + + public String getReasonClosed() + { + synchronized (reasonClosedLock) + { + return reasonClosed; + } + } + + public void setReasonClosed(String reasonClosed) + { + synchronized (reasonClosedLock) + { + if (this.reasonClosed == null) + this.reasonClosed = reasonClosed; + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java b/app/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java new file mode 100644 index 0000000..f88522c --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java @@ -0,0 +1,86 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; + +/** + * ChannelInputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelInputStream.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public final class ChannelInputStream extends InputStream +{ + Channel c; + + boolean isClosed = false; + boolean isEOF = false; + boolean extendedFlag = false; + + ChannelInputStream(Channel c, boolean isExtended) + { + this.c = c; + this.extendedFlag = isExtended; + } + + public int available() throws IOException + { + if (isEOF) + return 0; + + int avail = c.cm.getAvailable(c, extendedFlag); + + /* We must not return -1 on EOF */ + + return (avail > 0) ? avail : 0; + } + + public void close() throws IOException + { + isClosed = true; + } + + public int read(byte[] b, int off, int len) throws IOException + { + if (b == null) + throw new NullPointerException(); + + if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) + throw new IndexOutOfBoundsException(); + + if (len == 0) + return 0; + + if (isEOF) + return -1; + + int ret = c.cm.getChannelData(c, extendedFlag, b, off, len); + + if (ret == -1) + { + isEOF = true; + } + + return ret; + } + + public int read(byte[] b) throws IOException + { + return read(b, 0, b.length); + } + + public int read() throws IOException + { + /* Yes, this stream is pure and unbuffered, a single byte read() is slow */ + + final byte b[] = new byte[1]; + + int ret = read(b, 0, 1); + + if (ret != 1) + return -1; + + return b[0] & 0xff; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/ChannelManager.java b/app/src/main/java/com/trilead/ssh2/channel/ChannelManager.java new file mode 100644 index 0000000..88beffd --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/ChannelManager.java @@ -0,0 +1,1756 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Vector; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.ChannelCondition; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketChannelAuthAgentReq; +import com.trilead.ssh2.packets.PacketChannelOpenConfirmation; +import com.trilead.ssh2.packets.PacketChannelOpenFailure; +import com.trilead.ssh2.packets.PacketChannelTrileadPing; +import com.trilead.ssh2.packets.PacketGlobalCancelForwardRequest; +import com.trilead.ssh2.packets.PacketGlobalForwardRequest; +import com.trilead.ssh2.packets.PacketGlobalTrileadPing; +import com.trilead.ssh2.packets.PacketOpenDirectTCPIPChannel; +import com.trilead.ssh2.packets.PacketOpenSessionChannel; +import com.trilead.ssh2.packets.PacketSessionExecCommand; +import com.trilead.ssh2.packets.PacketSessionPtyRequest; +import com.trilead.ssh2.packets.PacketSessionPtyResize; +import com.trilead.ssh2.packets.PacketSessionStartShell; +import com.trilead.ssh2.packets.PacketSessionSubsystemRequest; +import com.trilead.ssh2.packets.PacketSessionX11Request; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.transport.MessageHandler; +import com.trilead.ssh2.transport.TransportManager; + +/** + * ChannelManager. Please read the comments in Channel.java. + *

+ * Besides the crypto part, this is the core of the library. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelManager.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $ + */ +public class ChannelManager implements MessageHandler +{ + private static final Logger log = Logger.getLogger(ChannelManager.class); + + private HashMap x11_magic_cookies = new HashMap(); + + private TransportManager tm; + + private Vector channels = new Vector(); + private int nextLocalChannel = 100; + private boolean shutdown = false; + private int globalSuccessCounter = 0; + private int globalFailedCounter = 0; + + private HashMap remoteForwardings = new HashMap(); + + private AuthAgentCallback authAgent; + + private Vector listenerThreads = new Vector(); + + private boolean listenerThreadsAllowed = true; + + public ChannelManager(TransportManager tm) + { + this.tm = tm; + tm.registerMessageHandler(this, 80, 100); + } + + private Channel getChannel(int id) + { + synchronized (channels) + { + for (int i = 0; i < channels.size(); i++) + { + Channel c = channels.elementAt(i); + if (c.localID == id) + return c; + } + } + return null; + } + + private void removeChannel(int id) + { + synchronized (channels) + { + for (int i = 0; i < channels.size(); i++) + { + Channel c = channels.elementAt(i); + if (c.localID == id) + { + channels.removeElementAt(i); + break; + } + } + } + } + + private int addChannel(Channel c) + { + synchronized (channels) + { + channels.addElement(c); + return nextLocalChannel++; + } + } + + private void waitUntilChannelOpen(Channel c) throws IOException + { + synchronized (c) + { + while (c.state == Channel.STATE_OPENING) + { + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + if (c.state != Channel.STATE_OPEN) + { + removeChannel(c.localID); + + String detail = c.getReasonClosed(); + + if (detail == null) + detail = "state: " + c.state; + + throw new IOException("Could not open channel (" + detail + ")"); + } + } + } + + private final boolean waitForGlobalRequestResult() throws IOException + { + synchronized (channels) + { + while ((globalSuccessCounter == 0) && (globalFailedCounter == 0)) + { + if (shutdown) + { + throw new IOException("The connection is being shutdown"); + } + + try + { + channels.wait(); + } + catch (InterruptedException ignore) + { + } + } + + if ((globalFailedCounter == 0) && (globalSuccessCounter == 1)) + return true; + + if ((globalFailedCounter == 1) && (globalSuccessCounter == 0)) + return false; + + throw new IOException("Illegal state. The server sent " + globalSuccessCounter + + " SSH_MSG_REQUEST_SUCCESS and " + globalFailedCounter + " SSH_MSG_REQUEST_FAILURE messages."); + } + } + + private final boolean waitForChannelRequestResult(Channel c) throws IOException + { + synchronized (c) + { + while ((c.successCounter == 0) && (c.failedCounter == 0)) + { + if (c.state != Channel.STATE_OPEN) + { + String detail = c.getReasonClosed(); + + if (detail == null) + detail = "state: " + c.state; + + throw new IOException("This SSH2 channel is not open (" + detail + ")"); + } + + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + if ((c.failedCounter == 0) && (c.successCounter == 1)) + return true; + + if ((c.failedCounter == 1) && (c.successCounter == 0)) + return false; + + throw new IOException("Illegal state. The server sent " + c.successCounter + + " SSH_MSG_CHANNEL_SUCCESS and " + c.failedCounter + " SSH_MSG_CHANNEL_FAILURE messages."); + } + } + + public void registerX11Cookie(String hexFakeCookie, X11ServerData data) + { + synchronized (x11_magic_cookies) + { + x11_magic_cookies.put(hexFakeCookie, data); + } + } + + public void unRegisterX11Cookie(String hexFakeCookie, boolean killChannels) + { + if (hexFakeCookie == null) + throw new IllegalStateException("hexFakeCookie may not be null"); + + synchronized (x11_magic_cookies) + { + x11_magic_cookies.remove(hexFakeCookie); + } + + if (killChannels == false) + return; + + if (log.isEnabled()) + log.log(50, "Closing all X11 channels for the given fake cookie"); + + Vector channel_copy; + + synchronized (channels) + { + channel_copy = (Vector) channels.clone(); + } + + for (int i = 0; i < channel_copy.size(); i++) + { + Channel c = channel_copy.elementAt(i); + + synchronized (c) + { + if (hexFakeCookie.equals(c.hexX11FakeCookie) == false) + continue; + } + + try + { + closeChannel(c, "Closing X11 channel since the corresponding session is closing", true); + } + catch (IOException e) + { + } + } + } + + public X11ServerData checkX11Cookie(String hexFakeCookie) + { + synchronized (x11_magic_cookies) + { + if (hexFakeCookie != null) + return x11_magic_cookies.get(hexFakeCookie); + } + return null; + } + + public void closeAllChannels() + { + if (log.isEnabled()) + log.log(50, "Closing all channels"); + + Vector channel_copy; + + synchronized (channels) + { + channel_copy = (Vector) channels.clone(); + } + + for (int i = 0; i < channel_copy.size(); i++) + { + Channel c = channel_copy.elementAt(i); + try + { + closeChannel(c, "Closing all channels", true); + } + catch (IOException e) + { + } + } + } + + public void closeChannel(Channel c, String reason, boolean force) throws IOException + { + byte msg[] = new byte[5]; + + synchronized (c) + { + if (force) + { + c.state = Channel.STATE_CLOSED; + c.EOF = true; + } + + c.setReasonClosed(reason); + + msg[0] = Packets.SSH_MSG_CHANNEL_CLOSE; + msg[1] = (byte) (c.remoteID >> 24); + msg[2] = (byte) (c.remoteID >> 16); + msg[3] = (byte) (c.remoteID >> 8); + msg[4] = (byte) (c.remoteID); + + c.notifyAll(); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent == true) + return; + tm.sendMessage(msg); + c.closeMessageSent = true; + } + + if (log.isEnabled()) + log.log(50, "Sent SSH_MSG_CHANNEL_CLOSE (channel " + c.localID + ")"); + } + + public void sendEOF(Channel c) throws IOException + { + byte[] msg = new byte[5]; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + return; + + msg[0] = Packets.SSH_MSG_CHANNEL_EOF; + msg[1] = (byte) (c.remoteID >> 24); + msg[2] = (byte) (c.remoteID >> 16); + msg[3] = (byte) (c.remoteID >> 8); + msg[4] = (byte) (c.remoteID); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent == true) + return; + tm.sendMessage(msg); + } + + if (log.isEnabled()) + log.log(50, "Sent EOF (Channel " + c.localID + "/" + c.remoteID + ")"); + } + + public void sendOpenConfirmation(Channel c) throws IOException + { + PacketChannelOpenConfirmation pcoc = null; + + synchronized (c) + { + if (c.state != Channel.STATE_OPENING) + return; + + c.state = Channel.STATE_OPEN; + + pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID, c.localWindow, c.localMaxPacketSize); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent == true) + return; + tm.sendMessage(pcoc.getPayload()); + } + } + + public void sendData(Channel c, byte[] buffer, int pos, int len) throws IOException + { + while (len > 0) + { + int thislen = 0; + byte[] msg; + + synchronized (c) + { + while (true) + { + if (c.state == Channel.STATE_CLOSED) + throw new IOException("SSH channel is closed. (" + c.getReasonClosed() + ")"); + + if (c.state != Channel.STATE_OPEN) + throw new IOException("SSH channel in strange state. (" + c.state + ")"); + + if (c.remoteWindow != 0) + break; + + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + /* len > 0, no sign extension can happen when comparing */ + + thislen = (c.remoteWindow >= len) ? len : (int) c.remoteWindow; + + int estimatedMaxDataLen = c.remoteMaxPacketSize - (tm.getPacketOverheadEstimate() + 9); + + /* The worst case scenario =) a true bottleneck */ + + if (estimatedMaxDataLen <= 0) + { + estimatedMaxDataLen = 1; + } + + if (thislen > estimatedMaxDataLen) + thislen = estimatedMaxDataLen; + + c.remoteWindow -= thislen; + + msg = new byte[1 + 8 + thislen]; + + msg[0] = Packets.SSH_MSG_CHANNEL_DATA; + msg[1] = (byte) (c.remoteID >> 24); + msg[2] = (byte) (c.remoteID >> 16); + msg[3] = (byte) (c.remoteID >> 8); + msg[4] = (byte) (c.remoteID); + msg[5] = (byte) (thislen >> 24); + msg[6] = (byte) (thislen >> 16); + msg[7] = (byte) (thislen >> 8); + msg[8] = (byte) (thislen); + + System.arraycopy(buffer, pos, msg, 9, thislen); + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent == true) + throw new IOException("SSH channel is closed. (" + c.getReasonClosed() + ")"); + + tm.sendMessage(msg); + } + + pos += thislen; + len -= thislen; + } + } + + public int requestGlobalForward(String bindAddress, int bindPort, String targetAddress, int targetPort) + throws IOException + { + RemoteForwardingData rfd = new RemoteForwardingData(); + + rfd.bindAddress = bindAddress; + rfd.bindPort = bindPort; + rfd.targetAddress = targetAddress; + rfd.targetPort = targetPort; + + synchronized (remoteForwardings) + { + Integer key = Integer.valueOf(bindPort); + + if (remoteForwardings.get(key) != null) + { + throw new IOException("There is already a forwarding for remote port " + bindPort); + } + + remoteForwardings.put(key, rfd); + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + PacketGlobalForwardRequest pgf = new PacketGlobalForwardRequest(true, bindAddress, bindPort); + tm.sendMessage(pgf.getPayload()); + + if (log.isEnabled()) + log.log(50, "Requesting a remote forwarding ('" + bindAddress + "', " + bindPort + ")"); + + try + { + if (waitForGlobalRequestResult() == false) + throw new IOException("The server denied the request (did you enable port forwarding?)"); + } + catch (IOException e) + { + synchronized (remoteForwardings) + { + remoteForwardings.remove(rfd); + } + throw e; + } + + return bindPort; + } + + public void requestCancelGlobalForward(int bindPort) throws IOException + { + RemoteForwardingData rfd = null; + + synchronized (remoteForwardings) + { + rfd = remoteForwardings.get(Integer.valueOf(bindPort)); + + if (rfd == null) + throw new IOException("Sorry, there is no known remote forwarding for remote port " + bindPort); + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + PacketGlobalCancelForwardRequest pgcf = new PacketGlobalCancelForwardRequest(true, rfd.bindAddress, + rfd.bindPort); + tm.sendMessage(pgcf.getPayload()); + + if (log.isEnabled()) + log.log(50, "Requesting cancelation of remote forward ('" + rfd.bindAddress + "', " + rfd.bindPort + ")"); + + try + { + if (waitForGlobalRequestResult() == false) + throw new IOException("The server denied the request."); + } + finally + { + synchronized (remoteForwardings) + { + /* Only now we are sure that no more forwarded connections will arrive */ + remoteForwardings.remove(rfd); + } + } + + } + + /** + * @param agent + * @throws IOException + */ + public boolean requestChannelAgentForwarding(Channel c, AuthAgentCallback authAgent) throws IOException { + synchronized (this) + { + if (this.authAgent != null) + throw new IllegalStateException("Auth agent already exists"); + + this.authAgent = authAgent; + } + + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + if (log.isEnabled()) + log.log(50, "Requesting agent forwarding"); + + PacketChannelAuthAgentReq aar = new PacketChannelAuthAgentReq(c.remoteID); + tm.sendMessage(aar.getPayload()); + + if (waitForChannelRequestResult(c) == false) { + authAgent = null; + return false; + } + + return true; + } + + public void registerThread(IChannelWorkerThread thr) throws IOException + { + synchronized (listenerThreads) + { + if (listenerThreadsAllowed == false) + throw new IOException("Too late, this connection is closed."); + listenerThreads.addElement(thr); + } + } + + public Channel openDirectTCPIPChannel(String host_to_connect, int port_to_connect, String originator_IP_address, + int originator_port) throws IOException + { + Channel c = new Channel(this); + + synchronized (c) + { + c.localID = addChannel(c); + // end of synchronized block forces writing out to main memory + } + + PacketOpenDirectTCPIPChannel dtc = new PacketOpenDirectTCPIPChannel(c.localID, c.localWindow, + c.localMaxPacketSize, host_to_connect, port_to_connect, originator_IP_address, originator_port); + + tm.sendMessage(dtc.getPayload()); + + waitUntilChannelOpen(c); + + return c; + } + + public Channel openSessionChannel() throws IOException + { + Channel c = new Channel(this); + + synchronized (c) + { + c.localID = addChannel(c); + // end of synchronized block forces the writing out to main memory + } + + if (log.isEnabled()) + log.log(50, "Sending SSH_MSG_CHANNEL_OPEN (Channel " + c.localID + ")"); + + PacketOpenSessionChannel smo = new PacketOpenSessionChannel(c.localID, c.localWindow, c.localMaxPacketSize); + tm.sendMessage(smo.getPayload()); + + waitUntilChannelOpen(c); + + return c; + } + + public void requestGlobalTrileadPing() throws IOException + { + synchronized (channels) + { + globalSuccessCounter = globalFailedCounter = 0; + } + + PacketGlobalTrileadPing pgtp = new PacketGlobalTrileadPing(); + + tm.sendMessage(pgtp.getPayload()); + + if (log.isEnabled()) + log.log(50, "Sending SSH_MSG_GLOBAL_REQUEST 'trilead-ping'."); + + try + { + if (waitForGlobalRequestResult() == true) + throw new IOException("Your server is alive - but buggy. " + + "It replied with SSH_MSG_REQUEST_SUCCESS when it actually should not."); + + } + catch (IOException e) + { + throw (IOException) new IOException("The ping request failed.").initCause(e); + } + } + + public void requestChannelTrileadPing(Channel c) throws IOException + { + PacketChannelTrileadPing pctp; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot ping this channel (" + c.getReasonClosed() + ")"); + + pctp = new PacketChannelTrileadPing(c.remoteID); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot ping this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(pctp.getPayload()); + } + + try + { + if (waitForChannelRequestResult(c) == true) + throw new IOException("Your server is alive - but buggy. " + + "It replied with SSH_MSG_SESSION_SUCCESS when it actually should not."); + + } + catch (IOException e) + { + throw (IOException) new IOException("The ping request failed.").initCause(e); + } + } + + public void requestPTY(Channel c, String term, int term_width_characters, int term_height_characters, + int term_width_pixels, int term_height_pixels, byte[] terminal_modes) throws IOException + { + PacketSessionPtyRequest spr; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")"); + + spr = new PacketSessionPtyRequest(c.remoteID, true, term, term_width_characters, term_height_characters, + term_width_pixels, term_height_pixels, terminal_modes); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot request PTY on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(spr.getPayload()); + } + + try + { + if (waitForChannelRequestResult(c) == false) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw (IOException) new IOException("PTY request failed").initCause(e); + } + } + + + public void resizePTY(Channel c, int term_width_characters, int term_height_characters, + int term_width_pixels, int term_height_pixels) throws IOException { + PacketSessionPtyResize spr; + + synchronized (c) { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request PTY on this channel (" + + c.getReasonClosed() + ")"); + + spr = new PacketSessionPtyResize(c.remoteID, term_width_characters, term_height_characters, + term_width_pixels, term_height_pixels); + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) { + if (c.closeMessageSent) + throw new IOException("Cannot request PTY on this channel (" + + c.getReasonClosed() + ")"); + tm.sendMessage(spr.getPayload()); + } + } + + + public void requestX11(Channel c, boolean singleConnection, String x11AuthenticationProtocol, + String x11AuthenticationCookie, int x11ScreenNumber) throws IOException + { + PacketSessionX11Request psr; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")"); + + psr = new PacketSessionX11Request(c.remoteID, true, singleConnection, x11AuthenticationProtocol, + x11AuthenticationCookie, x11ScreenNumber); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot request X11 on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(psr.getPayload()); + } + + if (log.isEnabled()) + log.log(50, "Requesting X11 forwarding (Channel " + c.localID + "/" + c.remoteID + ")"); + + try + { + if (waitForChannelRequestResult(c) == false) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw (IOException) new IOException("The X11 request failed.").initCause(e); + } + } + + public void requestSubSystem(Channel c, String subSystemName) throws IOException + { + PacketSessionSubsystemRequest ssr; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")"); + + ssr = new PacketSessionSubsystemRequest(c.remoteID, true, subSystemName); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot request subsystem on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(ssr.getPayload()); + } + + try + { + if (waitForChannelRequestResult(c) == false) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw (IOException) new IOException("The subsystem request failed.").initCause(e); + } + } + + public void requestExecCommand(Channel c, String cmd) throws IOException + { + PacketSessionExecCommand sm; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")"); + + sm = new PacketSessionExecCommand(c.remoteID, true, cmd); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot execute command on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(sm.getPayload()); + } + + if (log.isEnabled()) + log.log(50, "Executing command (channel " + c.localID + ", '" + cmd + "')"); + + try + { + if (waitForChannelRequestResult(c) == false) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw (IOException) new IOException("The execute request failed.").initCause(e); + } + } + + public void requestShell(Channel c) throws IOException + { + PacketSessionStartShell sm; + + synchronized (c) + { + if (c.state != Channel.STATE_OPEN) + throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")"); + + sm = new PacketSessionStartShell(c.remoteID, true); + + c.successCounter = c.failedCounter = 0; + } + + synchronized (c.channelSendLock) + { + if (c.closeMessageSent) + throw new IOException("Cannot start shell on this channel (" + c.getReasonClosed() + ")"); + tm.sendMessage(sm.getPayload()); + } + + try + { + if (waitForChannelRequestResult(c) == false) + throw new IOException("The server denied the request."); + } + catch (IOException e) + { + throw (IOException) new IOException("The shell request failed.").initCause(e); + } + } + + public void msgChannelExtendedData(byte[] msg, int msglen) throws IOException + { + if (msglen <= 13) + throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + int dataType = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); + int len = ((msg[9] & 0xff) << 24) | ((msg[10] & 0xff) << 16) | ((msg[11] & 0xff) << 8) | (msg[12] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_EXTENDED_DATA message for non-existent channel " + id); + + if (dataType != Packets.SSH_EXTENDED_DATA_STDERR) + throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has unknown type (" + dataType + ")"); + + if (len != (msglen - 13)) + throw new IOException("SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong len (calculated " + (msglen - 13) + + ", got " + len + ")"); + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_EXTENDED_DATA (channel " + id + ", " + len + ")"); + + synchronized (c) + { + if (c.state == Channel.STATE_CLOSED) + return; // ignore + + if (c.state != Channel.STATE_OPEN) + throw new IOException("Got SSH_MSG_CHANNEL_EXTENDED_DATA, but channel is not in correct state (" + + c.state + ")"); + + if (c.localWindow < len) + throw new IOException("Remote sent too much data, does not fit into window."); + + c.localWindow -= len; + + System.arraycopy(msg, 13, c.stderrBuffer, c.stderrWritepos, len); + c.stderrWritepos += len; + + c.notifyAll(); + } + } + + /** + * Wait until for a condition. + * + * @param c + * Channel + * @param timeout + * in ms, 0 means no timeout. + * @param condition_mask + * minimum event mask + * @return all current events + * + */ + public int waitForCondition(Channel c, long timeout, int condition_mask) + { + long end_time = 0; + boolean end_time_set = false; + + synchronized (c) + { + while (true) + { + int current_cond = 0; + + int stdoutAvail = c.stdoutWritepos - c.stdoutReadpos; + int stderrAvail = c.stderrWritepos - c.stderrReadpos; + + if (stdoutAvail > 0) + current_cond = current_cond | ChannelCondition.STDOUT_DATA; + + if (stderrAvail > 0) + current_cond = current_cond | ChannelCondition.STDERR_DATA; + + if (c.EOF) + current_cond = current_cond | ChannelCondition.EOF; + + if (c.getExitStatus() != null) + current_cond = current_cond | ChannelCondition.EXIT_STATUS; + + if (c.getExitSignal() != null) + current_cond = current_cond | ChannelCondition.EXIT_SIGNAL; + + if (c.state == Channel.STATE_CLOSED) + return current_cond | ChannelCondition.CLOSED | ChannelCondition.EOF; + + if ((current_cond & condition_mask) != 0) + return current_cond; + + if (timeout > 0) + { + if (!end_time_set) + { + end_time = System.currentTimeMillis() + timeout; + end_time_set = true; + } + else + { + timeout = end_time - System.currentTimeMillis(); + + if (timeout <= 0) + return current_cond | ChannelCondition.TIMEOUT; + } + } + + try + { + if (timeout > 0) + c.wait(timeout); + else + c.wait(); + } + catch (InterruptedException e) + { + } + } + } + } + + public int getAvailable(Channel c, boolean extended) throws IOException + { + synchronized (c) + { + int avail; + + if (extended) + avail = c.stderrWritepos - c.stderrReadpos; + else + avail = c.stdoutWritepos - c.stdoutReadpos; + + return ((avail > 0) ? avail : (c.EOF ? -1 : 0)); + } + } + + public int getChannelData(Channel c, boolean extended, byte[] target, int off, int len) throws IOException + { + int copylen = 0; + int increment = 0; + int remoteID = 0; + int localID = 0; + + synchronized (c) + { + int stdoutAvail = 0; + int stderrAvail = 0; + + while (true) + { + /* + * Data available? We have to return remaining data even if the + * channel is already closed. + */ + + stdoutAvail = c.stdoutWritepos - c.stdoutReadpos; + stderrAvail = c.stderrWritepos - c.stderrReadpos; + + if ((!extended) && (stdoutAvail != 0)) + break; + + if ((extended) && (stderrAvail != 0)) + break; + + /* Do not wait if more data will never arrive (EOF or CLOSED) */ + + if ((c.EOF) || (c.state != Channel.STATE_OPEN)) + return -1; + + try + { + c.wait(); + } + catch (InterruptedException ignore) + { + } + } + + /* OK, there is some data. Return it. */ + + if (!extended) + { + copylen = (stdoutAvail > len) ? len : stdoutAvail; + System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, target, off, copylen); + c.stdoutReadpos += copylen; + + if (c.stdoutReadpos != c.stdoutWritepos) + + System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, c.stdoutBuffer, 0, c.stdoutWritepos + - c.stdoutReadpos); + + c.stdoutWritepos -= c.stdoutReadpos; + c.stdoutReadpos = 0; + } + else + { + copylen = (stderrAvail > len) ? len : stderrAvail; + System.arraycopy(c.stderrBuffer, c.stderrReadpos, target, off, copylen); + c.stderrReadpos += copylen; + + if (c.stderrReadpos != c.stderrWritepos) + + System.arraycopy(c.stderrBuffer, c.stderrReadpos, c.stderrBuffer, 0, c.stderrWritepos + - c.stderrReadpos); + + c.stderrWritepos -= c.stderrReadpos; + c.stderrReadpos = 0; + } + + if (c.state != Channel.STATE_OPEN) + return copylen; + + if (c.localWindow < ((Channel.CHANNEL_BUFFER_SIZE + 1) / 2)) + { + int minFreeSpace = Math.min(Channel.CHANNEL_BUFFER_SIZE - c.stdoutWritepos, Channel.CHANNEL_BUFFER_SIZE + - c.stderrWritepos); + + increment = minFreeSpace - c.localWindow; + c.localWindow = minFreeSpace; + } + + remoteID = c.remoteID; /* read while holding the lock */ + localID = c.localID; /* read while holding the lock */ + } + + /* + * If a consumer reads stdout and stdin in parallel, we may end up with + * sending two msgWindowAdjust messages. Luckily, it + * does not matter in which order they arrive at the server. + */ + + if (increment > 0) + { + if (log.isEnabled()) + log.log(80, "Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + localID + ", " + increment + ")"); + + synchronized (c.channelSendLock) + { + byte[] msg = c.msgWindowAdjust; + + msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST; + msg[1] = (byte) (remoteID >> 24); + msg[2] = (byte) (remoteID >> 16); + msg[3] = (byte) (remoteID >> 8); + msg[4] = (byte) (remoteID); + msg[5] = (byte) (increment >> 24); + msg[6] = (byte) (increment >> 16); + msg[7] = (byte) (increment >> 8); + msg[8] = (byte) (increment); + + if (c.closeMessageSent == false) + tm.sendMessage(msg); + } + } + + return copylen; + } + + public void msgChannelData(byte[] msg, int msglen) throws IOException + { + if (msglen <= 9) + throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + int len = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_DATA message for non-existent channel " + id); + + if (len != (msglen - 9)) + throw new IOException("SSH_MSG_CHANNEL_DATA message has wrong len (calculated " + (msglen - 9) + ", got " + + len + ")"); + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_DATA (channel " + id + ", " + len + ")"); + + synchronized (c) + { + if (c.state == Channel.STATE_CLOSED) + return; // ignore + + if (c.state != Channel.STATE_OPEN) + throw new IOException("Got SSH_MSG_CHANNEL_DATA, but channel is not in correct state (" + c.state + ")"); + + if (c.localWindow < len) + throw new IOException("Remote sent too much data, does not fit into window."); + + c.localWindow -= len; + + System.arraycopy(msg, 9, c.stdoutBuffer, c.stdoutWritepos, len); + c.stdoutWritepos += len; + + c.notifyAll(); + } + } + + public void msgChannelWindowAdjust(byte[] msg, int msglen) throws IOException + { + if (msglen != 9) + throw new IOException("SSH_MSG_CHANNEL_WINDOW_ADJUST message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + int windowChange = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16) | ((msg[7] & 0xff) << 8) | (msg[8] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_WINDOW_ADJUST message for non-existent channel " + id); + + synchronized (c) + { + final long huge = 0xFFFFffffL; /* 2^32 - 1 */ + + c.remoteWindow += (windowChange & huge); /* avoid sign extension */ + + /* TODO - is this a good heuristic? */ + + if ((c.remoteWindow > huge)) + c.remoteWindow = huge; + + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + id + ", " + windowChange + ")"); + } + + public void msgChannelOpen(byte[] msg, int msglen) throws IOException + { + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + String channelType = tr.readString(); + int remoteID = tr.readUINT32(); /* sender channel */ + int remoteWindow = tr.readUINT32(); /* initial window size */ + int remoteMaxPacketSize = tr.readUINT32(); /* maximum packet size */ + + if ("x11".equals(channelType)) + { + synchronized (x11_magic_cookies) + { + /* If we did not request X11 forwarding, then simply ignore this bogus request. */ + + if (x11_magic_cookies.size() == 0) + { + PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, + Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "X11 forwarding not activated", ""); + + tm.sendAsynchronousMessage(pcof.getPayload()); + + if (log.isEnabled()) + log.log(20, "Unexpected X11 request, denying it!"); + + return; + } + } + + String remoteOriginatorAddress = tr.readString(); + int remoteOriginatorPort = tr.readUINT32(); + + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + /* + * The open confirmation message will be sent from another thread + */ + + RemoteX11AcceptThread rxat = new RemoteX11AcceptThread(c, remoteOriginatorAddress, remoteOriginatorPort); + rxat.setDaemon(true); + rxat.start(); + + return; + } + + if ("forwarded-tcpip".equals(channelType)) + { + String remoteConnectedAddress = tr.readString(); /* address that was connected */ + int remoteConnectedPort = tr.readUINT32(); /* port that was connected */ + String remoteOriginatorAddress = tr.readString(); /* originator IP address */ + int remoteOriginatorPort = tr.readUINT32(); /* originator port */ + + RemoteForwardingData rfd = null; + + synchronized (remoteForwardings) + { + rfd = remoteForwardings.get(Integer.valueOf(remoteConnectedPort)); + } + + if (rfd == null) + { + PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, + Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, + "No thanks, unknown port in forwarded-tcpip request", ""); + + /* Always try to be polite. */ + + tm.sendAsynchronousMessage(pcof.getPayload()); + + if (log.isEnabled()) + log.log(20, "Unexpected forwarded-tcpip request, denying it!"); + + return; + } + + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + /* + * The open confirmation message will be sent from another thread. + */ + + RemoteAcceptThread rat = new RemoteAcceptThread(c, remoteConnectedAddress, remoteConnectedPort, + remoteOriginatorAddress, remoteOriginatorPort, rfd.targetAddress, rfd.targetPort); + + rat.setDaemon(true); + rat.start(); + + return; + } + + if ("auth-agent@openssh.com".equals(channelType)) { + Channel c = new Channel(this); + + synchronized (c) + { + c.remoteID = remoteID; + c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */ + c.remoteMaxPacketSize = remoteMaxPacketSize; + c.localID = addChannel(c); + } + + AuthAgentForwardThread aat = new AuthAgentForwardThread(c, authAgent); + + aat.setDaemon(true); + aat.start(); + + return; + } + + /* Tell the server that we have no idea what it is talking about */ + + PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, + "Unknown channel type", ""); + + tm.sendAsynchronousMessage(pcof.getPayload()); + + if (log.isEnabled()) + log.log(20, "The peer tried to open an unsupported channel type (" + channelType + ")"); + } + + public void msgChannelRequest(byte[] msg, int msglen) throws IOException + { + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + int id = tr.readUINT32(); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_REQUEST message for non-existent channel " + id); + + String type = tr.readString("US-ASCII"); + boolean wantReply = tr.readBoolean(); + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_REQUEST (channel " + id + ", '" + type + "')"); + + if (type.equals("exit-status")) + { + if (wantReply != false) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message, 'want reply' is true"); + + int exit_status = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); + + synchronized (c) + { + c.exit_status = Integer.valueOf(exit_status); + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got EXIT STATUS (channel " + id + ", status " + exit_status + ")"); + + return; + } + + if (type.equals("exit-signal")) + { + if (wantReply != false) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message, 'want reply' is true"); + + String signame = tr.readString("US-ASCII"); + tr.readBoolean(); + tr.readString(); + tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Badly formatted SSH_MSG_CHANNEL_REQUEST message"); + + synchronized (c) + { + c.exit_signal = signame; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got EXIT SIGNAL (channel " + id + ", signal " + signame + ")"); + + return; + } + + /* We simply ignore unknown channel requests, however, if the server wants a reply, + * then we signal that we have no idea what it is about. + */ + + if (wantReply) + { + byte[] reply = new byte[5]; + + reply[0] = Packets.SSH_MSG_CHANNEL_FAILURE; + reply[1] = (byte) (c.remoteID >> 24); + reply[2] = (byte) (c.remoteID >> 16); + reply[3] = (byte) (c.remoteID >> 8); + reply[4] = (byte) (c.remoteID); + + tm.sendAsynchronousMessage(reply); + } + + if (log.isEnabled()) + log.log(50, "Channel request '" + type + "' is not known, ignoring it"); + } + + public void msgChannelEOF(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_EOF message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_EOF message for non-existent channel " + id); + + synchronized (c) + { + c.EOF = true; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_EOF (channel " + id + ")"); + } + + public void msgChannelClose(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_CLOSE message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_CLOSE message for non-existent channel " + id); + + synchronized (c) + { + c.EOF = true; + c.state = Channel.STATE_CLOSED; + c.setReasonClosed("Close requested by remote"); + c.closeMessageRecv = true; + + removeChannel(c.localID); + + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_CLOSE (channel " + id + ")"); + } + + public void msgChannelSuccess(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_SUCCESS message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_SUCCESS message for non-existent channel " + id); + + synchronized (c) + { + c.successCounter++; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_CHANNEL_SUCCESS (channel " + id + ")"); + } + + public void msgChannelFailure(byte[] msg, int msglen) throws IOException + { + if (msglen != 5) + throw new IOException("SSH_MSG_CHANNEL_FAILURE message has wrong size (" + msglen + ")"); + + int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16) | ((msg[3] & 0xff) << 8) | (msg[4] & 0xff); + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_FAILURE message for non-existent channel " + id); + + synchronized (c) + { + c.failedCounter++; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_FAILURE (channel " + id + ")"); + } + + public void msgChannelOpenConfirmation(byte[] msg, int msglen) throws IOException + { + PacketChannelOpenConfirmation sm = new PacketChannelOpenConfirmation(msg, 0, msglen); + + Channel c = getChannel(sm.recipientChannelID); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for non-existent channel " + + sm.recipientChannelID); + + synchronized (c) + { + if (c.state != Channel.STATE_OPENING) + throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for channel " + + sm.recipientChannelID); + + c.remoteID = sm.senderChannelID; + c.remoteWindow = sm.initialWindowSize & 0xFFFFffffL; /* convert UINT32 to long */ + c.remoteMaxPacketSize = sm.maxPacketSize; + c.state = Channel.STATE_OPEN; + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_OPEN_CONFIRMATION (channel " + sm.recipientChannelID + " / remote: " + + sm.senderChannelID + ")"); + } + + public void msgChannelOpenFailure(byte[] msg, int msglen) throws IOException + { + if (msglen < 5) + throw new IOException("SSH_MSG_CHANNEL_OPEN_FAILURE message has wrong size (" + msglen + ")"); + + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + int id = tr.readUINT32(); /* sender channel */ + + Channel c = getChannel(id); + + if (c == null) + throw new IOException("Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE message for non-existent channel " + id); + + int reasonCode = tr.readUINT32(); + String description = tr.readString("UTF-8"); + + String reasonCodeSymbolicName = null; + + switch (reasonCode) + { + case 1: + reasonCodeSymbolicName = "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED"; + break; + case 2: + reasonCodeSymbolicName = "SSH_OPEN_CONNECT_FAILED"; + break; + case 3: + reasonCodeSymbolicName = "SSH_OPEN_UNKNOWN_CHANNEL_TYPE"; + break; + case 4: + reasonCodeSymbolicName = "SSH_OPEN_RESOURCE_SHORTAGE"; + break; + default: + reasonCodeSymbolicName = "UNKNOWN REASON CODE (" + reasonCode + ")"; + } + + StringBuffer descriptionBuffer = new StringBuffer(); + descriptionBuffer.append(description); + + for (int i = 0; i < descriptionBuffer.length(); i++) + { + char cc = descriptionBuffer.charAt(i); + + if ((cc >= 32) && (cc <= 126)) + continue; + descriptionBuffer.setCharAt(i, '\uFFFD'); + } + + synchronized (c) + { + c.EOF = true; + c.state = Channel.STATE_CLOSED; + c.setReasonClosed("The server refused to open the channel (" + reasonCodeSymbolicName + ", '" + + descriptionBuffer.toString() + "')"); + c.notifyAll(); + } + + if (log.isEnabled()) + log.log(50, "Got SSH_MSG_CHANNEL_OPEN_FAILURE (channel " + id + ")"); + } + + public void msgGlobalRequest(byte[] msg, int msglen) throws IOException + { + /* Currently we do not support any kind of global request */ + + TypesReader tr = new TypesReader(msg, 0, msglen); + + tr.readByte(); // skip packet type + String requestName = tr.readString(); + boolean wantReply = tr.readBoolean(); + + if (wantReply) + { + byte[] reply_failure = new byte[1]; + reply_failure[0] = Packets.SSH_MSG_REQUEST_FAILURE; + + tm.sendAsynchronousMessage(reply_failure); + } + + /* We do not clean up the requestName String - that is OK for debug */ + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_GLOBAL_REQUEST (" + requestName + ")"); + } + + public void msgGlobalSuccess() throws IOException + { + synchronized (channels) + { + globalSuccessCounter++; + channels.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_REQUEST_SUCCESS"); + } + + public void msgGlobalFailure() throws IOException + { + synchronized (channels) + { + globalFailedCounter++; + channels.notifyAll(); + } + + if (log.isEnabled()) + log.log(80, "Got SSH_MSG_REQUEST_FAILURE"); + } + + public void handleMessage(byte[] msg, int msglen) throws IOException + { + if (msg == null) + { + if (log.isEnabled()) + log.log(50, "HandleMessage: got shutdown"); + + synchronized (listenerThreads) + { + for (int i = 0; i < listenerThreads.size(); i++) + { + IChannelWorkerThread lat = listenerThreads.elementAt(i); + lat.stopWorking(); + } + listenerThreadsAllowed = false; + } + + synchronized (channels) + { + shutdown = true; + + for (int i = 0; i < channels.size(); i++) + { + Channel c = channels.elementAt(i); + synchronized (c) + { + c.EOF = true; + c.state = Channel.STATE_CLOSED; + c.setReasonClosed("The connection is being shutdown"); + c.closeMessageRecv = true; /* + * You never know, perhaps + * we are waiting for a + * pending close message + * from the server... + */ + c.notifyAll(); + } + } + /* Works with J2ME */ + channels.setSize(0); + channels.trimToSize(); + channels.notifyAll(); /* Notify global response waiters */ + return; + } + } + + switch (msg[0]) + { + case Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION: + msgChannelOpenConfirmation(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST: + msgChannelWindowAdjust(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_DATA: + msgChannelData(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_EXTENDED_DATA: + msgChannelExtendedData(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_REQUEST: + msgChannelRequest(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_EOF: + msgChannelEOF(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_OPEN: + msgChannelOpen(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_CLOSE: + msgChannelClose(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_SUCCESS: + msgChannelSuccess(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_FAILURE: + msgChannelFailure(msg, msglen); + break; + case Packets.SSH_MSG_CHANNEL_OPEN_FAILURE: + msgChannelOpenFailure(msg, msglen); + break; + case Packets.SSH_MSG_GLOBAL_REQUEST: + msgGlobalRequest(msg, msglen); + break; + case Packets.SSH_MSG_REQUEST_SUCCESS: + msgGlobalSuccess(); + break; + case Packets.SSH_MSG_REQUEST_FAILURE: + msgGlobalFailure(); + break; + default: + throw new IOException("Cannot handle unknown channel message " + (msg[0] & 0xff)); + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java b/app/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java new file mode 100644 index 0000000..c1d56e8 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java @@ -0,0 +1,71 @@ +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * ChannelOutputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ChannelOutputStream.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public final class ChannelOutputStream extends OutputStream +{ + Channel c; + + private byte[] writeBuffer; + + boolean isClosed = false; + + ChannelOutputStream(Channel c) + { + this.c = c; + writeBuffer = new byte[1]; + } + + public void write(int b) throws IOException + { + writeBuffer[0] = (byte) b; + + write(writeBuffer, 0, 1); + } + + public void close() throws IOException + { + if (isClosed == false) + { + isClosed = true; + c.cm.sendEOF(c); + } + } + + public void flush() throws IOException + { + if (isClosed) + throw new IOException("This OutputStream is closed."); + + /* This is a no-op, since this stream is unbuffered */ + } + + public void write(byte[] b, int off, int len) throws IOException + { + if (isClosed) + throw new IOException("This OutputStream is closed."); + + if (b == null) + throw new NullPointerException(); + + if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) + throw new IndexOutOfBoundsException(); + + if (len == 0) + return; + + c.cm.sendData(c, b, off, len); + } + + public void write(byte[] b) throws IOException + { + write(b, 0, b.length); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java b/app/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java new file mode 100644 index 0000000..ef3a3d0 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java @@ -0,0 +1,284 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.PushbackInputStream; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NoRouteToHostException; +import java.net.ServerSocket; +import java.net.Socket; + +import net.sourceforge.jsocks.Proxy; +import net.sourceforge.jsocks.ProxyMessage; +import net.sourceforge.jsocks.Socks4Message; +import net.sourceforge.jsocks.Socks5Message; +import net.sourceforge.jsocks.SocksException; +import net.sourceforge.jsocks.server.ServerAuthenticator; +import net.sourceforge.jsocks.server.ServerAuthenticatorNone; + +/** + * DynamicAcceptThread. + * + * @author Kenny Root + * @version $Id$ + */ +public class DynamicAcceptThread extends Thread implements IChannelWorkerThread { + private ChannelManager cm; + private ServerSocket ss; + + class DynamicAcceptRunnable implements Runnable { + private static final int idleTimeout = 180000; //3 minutes + + private ServerAuthenticator auth; + private Socket sock; + private InputStream in; + private OutputStream out; + private ProxyMessage msg; + + public DynamicAcceptRunnable(ServerAuthenticator auth, Socket sock) { + this.auth = auth; + this.sock = sock; + + setName("DynamicAcceptRunnable"); + } + + public void run() { + try { + startSession(); + } catch (IOException ioe) { + int error_code = Proxy.SOCKS_FAILURE; + + if (ioe instanceof SocksException) + error_code = ((SocksException) ioe).errCode; + else if (ioe instanceof NoRouteToHostException) + error_code = Proxy.SOCKS_HOST_UNREACHABLE; + else if (ioe instanceof ConnectException) + error_code = Proxy.SOCKS_CONNECTION_REFUSED; + else if (ioe instanceof InterruptedIOException) + error_code = Proxy.SOCKS_TTL_EXPIRE; + + if (error_code > Proxy.SOCKS_ADDR_NOT_SUPPORTED + || error_code < 0) { + error_code = Proxy.SOCKS_FAILURE; + } + + sendErrorMessage(error_code); + } finally { + if (auth != null) + auth.endSession(); + } + } + + private ProxyMessage readMsg(InputStream in) throws IOException { + PushbackInputStream push_in; + if (in instanceof PushbackInputStream) + push_in = (PushbackInputStream) in; + else + push_in = new PushbackInputStream(in); + + int version = push_in.read(); + push_in.unread(version); + + ProxyMessage msg; + + if (version == 5) { + msg = new Socks5Message(push_in, false); + } else if (version == 4) { + msg = new Socks4Message(push_in, false); + } else { + throw new SocksException(Proxy.SOCKS_FAILURE); + } + return msg; + } + + private void sendErrorMessage(int error_code) { + ProxyMessage err_msg; + if (msg instanceof Socks4Message) + err_msg = new Socks4Message(Socks4Message.REPLY_REJECTED); + else + err_msg = new Socks5Message(error_code); + try { + err_msg.write(out); + } catch (IOException ioe) { + } + } + + private void handleRequest(ProxyMessage msg) throws IOException { + if (!auth.checkRequest(msg)) + throw new SocksException(Proxy.SOCKS_FAILURE); + + switch (msg.command) { + case Proxy.SOCKS_CMD_CONNECT: + onConnect(msg); + break; + default: + throw new SocksException(Proxy.SOCKS_CMD_NOT_SUPPORTED); + } + } + + private void startSession() throws IOException { + sock.setSoTimeout(idleTimeout); + + try { + auth = auth.startSession(sock); + } catch (IOException ioe) { + System.out.println("Could not start SOCKS session"); + ioe.printStackTrace(); + auth = null; + return; + } + + if (auth == null) { // Authentication failed + System.out.println("SOCKS auth failed"); + return; + } + + in = auth.getInputStream(); + out = auth.getOutputStream(); + + msg = readMsg(in); + handleRequest(msg); + } + + private void onConnect(ProxyMessage msg) throws IOException { + ProxyMessage response = null; + Channel cn = null; + StreamForwarder r2l = null; + StreamForwarder l2r = null; + + if (msg instanceof Socks5Message) { + response = new Socks5Message(Proxy.SOCKS_SUCCESS, (InetAddress)null, 0); + } else { + response = new Socks4Message(Socks4Message.REPLY_OK, (InetAddress)null, 0); + } + response.write(out); + + String destHost = msg.host; + if (msg.ip != null) + destHost = msg.ip.getHostAddress(); + + try { + /* + * This may fail, e.g., if the remote port is closed (in + * optimistic terms: not open yet) + */ + + cn = cm.openDirectTCPIPChannel(destHost, msg.port, + "127.0.0.1", 0); + + } catch (IOException e) { + /* + * Simply close the local socket and wait for the next incoming + * connection + */ + + try { + sock.close(); + } catch (IOException ignore) { + } + + return; + } + + try { + r2l = new StreamForwarder(cn, null, sock, cn.stdoutStream, out, "RemoteToLocal"); + l2r = new StreamForwarder(cn, r2l, sock, in, cn.stdinStream, "LocalToRemote"); + } catch (IOException e) { + try { + /* + * This message is only visible during debugging, since we + * discard the channel immediatelly + */ + cn.cm.closeChannel(cn, + "Weird error during creation of StreamForwarder (" + + e.getMessage() + ")", true); + } catch (IOException ignore) { + } + + return; + } + + r2l.setDaemon(true); + l2r.setDaemon(true); + r2l.start(); + l2r.start(); + } + } + + public DynamicAcceptThread(ChannelManager cm, int local_port) + throws IOException { + this.cm = cm; + + setName("DynamicAcceptThread"); + + ss = new ServerSocket(local_port); + } + + public DynamicAcceptThread(ChannelManager cm, InetSocketAddress localAddress) + throws IOException { + this.cm = cm; + + ss = new ServerSocket(); + ss.bind(localAddress); + } + + @Override + public void run() { + try { + cm.registerThread(this); + } catch (IOException e) { + stopWorking(); + return; + } + + while (true) { + Socket sock = null; + + try { + sock = ss.accept(); + } catch (IOException e) { + stopWorking(); + return; + } + + DynamicAcceptRunnable dar = new DynamicAcceptRunnable(new ServerAuthenticatorNone(), sock); + Thread t = new Thread(dar); + t.setDaemon(true); + t.start(); + } + } + + /* + * (non-Javadoc) + * + * @see com.trilead.ssh2.channel.IChannelWorkerThread#stopWorking() + */ + public void stopWorking() { + try { + /* This will lead to an IOException in the ss.accept() call */ + ss.close(); + } catch (IOException e) { + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java b/app/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java new file mode 100644 index 0000000..bce9b1b --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java @@ -0,0 +1,13 @@ + +package com.trilead.ssh2.channel; + +/** + * IChannelWorkerThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: IChannelWorkerThread.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +interface IChannelWorkerThread +{ + public void stopWorking(); +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java b/app/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java new file mode 100644 index 0000000..0d1bb35 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java @@ -0,0 +1,135 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * LocalAcceptThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: LocalAcceptThread.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class LocalAcceptThread extends Thread implements IChannelWorkerThread +{ + ChannelManager cm; + String host_to_connect; + int port_to_connect; + + final ServerSocket ss; + + public LocalAcceptThread(ChannelManager cm, int local_port, String host_to_connect, int port_to_connect) + throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + ss = new ServerSocket(local_port); + } + + public LocalAcceptThread(ChannelManager cm, InetSocketAddress localAddress, String host_to_connect, + int port_to_connect) throws IOException + { + this.cm = cm; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + + ss = new ServerSocket(); + ss.bind(localAddress); + } + + public void run() + { + try + { + cm.registerThread(this); + } + catch (IOException e) + { + stopWorking(); + return; + } + + while (true) + { + Socket s = null; + + try + { + s = ss.accept(); + } + catch (IOException e) + { + stopWorking(); + return; + } + + Channel cn = null; + StreamForwarder r2l = null; + StreamForwarder l2r = null; + + try + { + /* This may fail, e.g., if the remote port is closed (in optimistic terms: not open yet) */ + + cn = cm.openDirectTCPIPChannel(host_to_connect, port_to_connect, s.getInetAddress().getHostAddress(), s + .getPort()); + + } + catch (IOException e) + { + /* Simply close the local socket and wait for the next incoming connection */ + + try + { + s.close(); + } + catch (IOException ignore) + { + } + + continue; + } + + try + { + r2l = new StreamForwarder(cn, null, s, cn.stdoutStream, s.getOutputStream(), "RemoteToLocal"); + l2r = new StreamForwarder(cn, r2l, s, s.getInputStream(), cn.stdinStream, "LocalToRemote"); + } + catch (IOException e) + { + try + { + /* This message is only visible during debugging, since we discard the channel immediatelly */ + cn.cm.closeChannel(cn, "Weird error during creation of StreamForwarder (" + e.getMessage() + ")", + true); + } + catch (IOException ignore) + { + } + + continue; + } + + r2l.setDaemon(true); + l2r.setDaemon(true); + r2l.start(); + l2r.start(); + } + } + + public void stopWorking() + { + try + { + /* This will lead to an IOException in the ss.accept() call */ + ss.close(); + } + catch (IOException e) + { + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java b/app/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java new file mode 100644 index 0000000..29b02b8 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java @@ -0,0 +1,103 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.net.Socket; + +import com.trilead.ssh2.log.Logger; + + +/** + * RemoteAcceptThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RemoteAcceptThread.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class RemoteAcceptThread extends Thread +{ + private static final Logger log = Logger.getLogger(RemoteAcceptThread.class); + + Channel c; + + String remoteConnectedAddress; + int remoteConnectedPort; + String remoteOriginatorAddress; + int remoteOriginatorPort; + String targetAddress; + int targetPort; + + Socket s; + + public RemoteAcceptThread(Channel c, String remoteConnectedAddress, int remoteConnectedPort, + String remoteOriginatorAddress, int remoteOriginatorPort, String targetAddress, int targetPort) + { + this.c = c; + this.remoteConnectedAddress = remoteConnectedAddress; + this.remoteConnectedPort = remoteConnectedPort; + this.remoteOriginatorAddress = remoteOriginatorAddress; + this.remoteOriginatorPort = remoteOriginatorPort; + this.targetAddress = targetAddress; + this.targetPort = targetPort; + + if (log.isEnabled()) + log.log(20, "RemoteAcceptThread: " + remoteConnectedAddress + "/" + remoteConnectedPort + ", R: " + + remoteOriginatorAddress + "/" + remoteOriginatorPort); + } + + public void run() + { + try + { + c.cm.sendOpenConfirmation(c); + + s = new Socket(targetAddress, targetPort); + + StreamForwarder r2l = new StreamForwarder(c, null, s, c.getStdoutStream(), s.getOutputStream(), + "RemoteToLocal"); + StreamForwarder l2r = new StreamForwarder(c, null, null, s.getInputStream(), c.getStdinStream(), + "LocalToRemote"); + + /* No need to start two threads, one can be executed in the current thread */ + + r2l.setDaemon(true); + r2l.start(); + l2r.run(); + + while (r2l.isAlive()) + { + try + { + r2l.join(); + } + catch (InterruptedException e) + { + } + } + + /* If the channel is already closed, then this is a no-op */ + + c.cm.closeChannel(c, "EOF on both streams reached.", true); + s.close(); + } + catch (IOException e) + { + log.log(50, "IOException in proxy code: " + e.getMessage()); + + try + { + c.cm.closeChannel(c, "IOException in proxy code (" + e.getMessage() + ")", true); + } + catch (IOException e1) + { + } + try + { + if (s != null) + s.close(); + } + catch (IOException e1) + { + } + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java b/app/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java new file mode 100644 index 0000000..d05378e --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java @@ -0,0 +1,17 @@ + +package com.trilead.ssh2.channel; + +/** + * RemoteForwardingData. Data about a requested remote forwarding. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RemoteForwardingData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class RemoteForwardingData +{ + public String bindAddress; + public int bindPort; + + String targetAddress; + int targetPort; +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java b/app/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java new file mode 100644 index 0000000..9f99410 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java @@ -0,0 +1,240 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +import com.trilead.ssh2.log.Logger; + + +/** + * RemoteX11AcceptThread. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RemoteX11AcceptThread.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class RemoteX11AcceptThread extends Thread +{ + private static final Logger log = Logger.getLogger(RemoteX11AcceptThread.class); + + Channel c; + + String remoteOriginatorAddress; + int remoteOriginatorPort; + + Socket s; + + public RemoteX11AcceptThread(Channel c, String remoteOriginatorAddress, int remoteOriginatorPort) + { + this.c = c; + this.remoteOriginatorAddress = remoteOriginatorAddress; + this.remoteOriginatorPort = remoteOriginatorPort; + } + + public void run() + { + try + { + /* Send Open Confirmation */ + + c.cm.sendOpenConfirmation(c); + + /* Read startup packet from client */ + + OutputStream remote_os = c.getStdinStream(); + InputStream remote_is = c.getStdoutStream(); + + /* The following code is based on the protocol description given in: + * Scheifler/Gettys, + * X Windows System: Core and Extension Protocols: + * X Version 11, Releases 6 and 6.1 ISBN 1-55558-148-X + */ + + /* + * Client startup: + * + * 1 0X42 MSB first/0x6c lSB first - byteorder + * 1 - unused + * 2 card16 - protocol-major-version + * 2 card16 - protocol-minor-version + * 2 n - lenght of authorization-protocol-name + * 2 d - lenght of authorization-protocol-data + * 2 - unused + * string8 - authorization-protocol-name + * p - unused, p=pad(n) + * string8 - authorization-protocol-data + * q - unused, q=pad(d) + * + * pad(X) = (4 - (X mod 4)) mod 4 + * + * Server response: + * + * 1 (0 failed, 2 authenticate, 1 success) + * ... + * + */ + + /* Later on we will simply forward the first 6 header bytes to the "real" X11 server */ + + byte[] header = new byte[6]; + + if (remote_is.read(header) != 6) + throw new IOException("Unexpected EOF on X11 startup!"); + + if ((header[0] != 0x42) && (header[0] != 0x6c)) // 0x42 MSB first, 0x6C LSB first + throw new IOException("Unknown endian format in X11 message!"); + + /* Yes, I came up with this myself - shall I file an application for a patent? =) */ + + int idxMSB = (header[0] == 0x42) ? 0 : 1; + + /* Read authorization data header */ + + byte[] auth_buff = new byte[6]; + + if (remote_is.read(auth_buff) != 6) + throw new IOException("Unexpected EOF on X11 startup!"); + + int authProtocolNameLength = ((auth_buff[idxMSB] & 0xff) << 8) | (auth_buff[1 - idxMSB] & 0xff); + int authProtocolDataLength = ((auth_buff[2 + idxMSB] & 0xff) << 8) | (auth_buff[3 - idxMSB] & 0xff); + + if ((authProtocolNameLength > 256) || (authProtocolDataLength > 256)) + throw new IOException("Buggy X11 authorization data"); + + int authProtocolNamePadding = ((4 - (authProtocolNameLength % 4)) % 4); + int authProtocolDataPadding = ((4 - (authProtocolDataLength % 4)) % 4); + + byte[] authProtocolName = new byte[authProtocolNameLength]; + byte[] authProtocolData = new byte[authProtocolDataLength]; + + byte[] paddingBuffer = new byte[4]; + + if (remote_is.read(authProtocolName) != authProtocolNameLength) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolName)"); + + if (remote_is.read(paddingBuffer, 0, authProtocolNamePadding) != authProtocolNamePadding) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolNamePadding)"); + + if (remote_is.read(authProtocolData) != authProtocolDataLength) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolData)"); + + if (remote_is.read(paddingBuffer, 0, authProtocolDataPadding) != authProtocolDataPadding) + throw new IOException("Unexpected EOF on X11 startup! (authProtocolDataPadding)"); + + if ("MIT-MAGIC-COOKIE-1".equals(new String(authProtocolName, "ISO-8859-1")) == false) + throw new IOException("Unknown X11 authorization protocol!"); + + if (authProtocolDataLength != 16) + throw new IOException("Wrong data length for X11 authorization data!"); + + StringBuffer tmp = new StringBuffer(32); + for (int i = 0; i < authProtocolData.length; i++) + { + String digit2 = Integer.toHexString(authProtocolData[i] & 0xff); + tmp.append((digit2.length() == 2) ? digit2 : "0" + digit2); + } + String hexEncodedFakeCookie = tmp.toString(); + + /* Order is very important here - it may be that a certain x11 forwarding + * gets disabled right in the moment when we check and register our connection + * */ + + synchronized (c) + { + /* Please read the comment in Channel.java */ + c.hexX11FakeCookie = hexEncodedFakeCookie; + } + + /* Now check our fake cookie directory to see if we produced this cookie */ + + X11ServerData sd = c.cm.checkX11Cookie(hexEncodedFakeCookie); + + if (sd == null) + throw new IOException("Invalid X11 cookie received."); + + /* If the session which corresponds to this cookie is closed then we will + * detect this: the session's close code will close all channels + * with the session's assigned x11 fake cookie. + */ + + s = new Socket(sd.hostname, sd.port); + + OutputStream x11_os = s.getOutputStream(); + InputStream x11_is = s.getInputStream(); + + /* Now we are sending the startup packet to the real X11 server */ + + x11_os.write(header); + + if (sd.x11_magic_cookie == null) + { + byte[] emptyAuthData = new byte[6]; + /* empty auth data, hopefully you are connecting to localhost =) */ + x11_os.write(emptyAuthData); + } + else + { + if (sd.x11_magic_cookie.length != 16) + throw new IOException("The real X11 cookie has an invalid length!"); + + /* send X11 cookie specified by client */ + x11_os.write(auth_buff); + x11_os.write(authProtocolName); /* re-use */ + x11_os.write(paddingBuffer, 0, authProtocolNamePadding); + x11_os.write(sd.x11_magic_cookie); + x11_os.write(paddingBuffer, 0, authProtocolDataPadding); + } + + x11_os.flush(); + + /* Start forwarding traffic */ + + StreamForwarder r2l = new StreamForwarder(c, null, s, remote_is, x11_os, "RemoteToX11"); + StreamForwarder l2r = new StreamForwarder(c, null, null, x11_is, remote_os, "X11ToRemote"); + + /* No need to start two threads, one can be executed in the current thread */ + + r2l.setDaemon(true); + r2l.start(); + l2r.run(); + + while (r2l.isAlive()) + { + try + { + r2l.join(); + } + catch (InterruptedException e) + { + } + } + + /* If the channel is already closed, then this is a no-op */ + + c.cm.closeChannel(c, "EOF on both X11 streams reached.", true); + s.close(); + } + catch (IOException e) + { + log.log(50, "IOException in X11 proxy code: " + e.getMessage()); + + try + { + c.cm.closeChannel(c, "IOException in X11 proxy code (" + e.getMessage() + ")", true); + } + catch (IOException e1) + { + } + try + { + if (s != null) + s.close(); + } + catch (IOException e1) + { + } + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java b/app/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java new file mode 100644 index 0000000..e1afee8 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java @@ -0,0 +1,113 @@ + +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +/** + * A StreamForwarder forwards data between two given streams. + * If two StreamForwarder threads are used (one for each direction) + * then one can be configured to shutdown the underlying channel/socket + * if both threads have finished forwarding (EOF). + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: StreamForwarder.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class StreamForwarder extends Thread +{ + final OutputStream os; + final InputStream is; + final byte[] buffer = new byte[Channel.CHANNEL_BUFFER_SIZE]; + final Channel c; + final StreamForwarder sibling; + final Socket s; + final String mode; + + StreamForwarder(Channel c, StreamForwarder sibling, Socket s, InputStream is, OutputStream os, String mode) + throws IOException + { + this.is = is; + this.os = os; + this.mode = mode; + this.c = c; + this.sibling = sibling; + this.s = s; + } + + public void run() + { + try + { + while (true) + { + int len = is.read(buffer); + if (len <= 0) + break; + os.write(buffer, 0, len); + os.flush(); + } + } + catch (IOException ignore) + { + try + { + c.cm.closeChannel(c, "Closed due to exception in StreamForwarder (" + mode + "): " + + ignore.getMessage(), true); + } + catch (IOException e) + { + } + } + finally + { + try + { + os.close(); + } + catch (IOException e1) + { + } + try + { + is.close(); + } + catch (IOException e2) + { + } + + if (sibling != null) + { + while (sibling.isAlive()) + { + try + { + sibling.join(); + } + catch (InterruptedException e) + { + } + } + + try + { + c.cm.closeChannel(c, "StreamForwarder (" + mode + ") is cleaning up the connection", true); + } + catch (IOException e3) + { + } + } + + if (s != null) { + try + { + s.close(); + } + catch (IOException e1) + { + } + } + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/channel/X11ServerData.java b/app/src/main/java/com/trilead/ssh2/channel/X11ServerData.java new file mode 100644 index 0000000..041f9cb --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/channel/X11ServerData.java @@ -0,0 +1,16 @@ + +package com.trilead.ssh2.channel; + +/** + * X11ServerData. Data regarding an x11 forwarding target. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: X11ServerData.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + * + */ +public class X11ServerData +{ + public String hostname; + public int port; + public byte[] x11_magic_cookie; /* not the remote (fake) one, the local (real) one */ +} diff --git a/app/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java b/app/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java new file mode 100644 index 0000000..9f8d7ef --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java @@ -0,0 +1,96 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2.compression; + +import java.util.Vector; + +/** + * @author Kenny Root + * + */ +public class CompressionFactory { + static class CompressorEntry + { + String type; + String compressorClass; + + public CompressorEntry(String type, String compressorClass) + { + this.type = type; + this.compressorClass = compressorClass; + } + } + + static Vector compressors = new Vector(); + + static + { + /* Higher Priority First */ + + compressors.addElement(new CompressorEntry("zlib", "com.trilead.ssh2.compression.Zlib")); + compressors.addElement(new CompressorEntry("zlib@openssh.com", "com.trilead.ssh2.compression.ZlibOpenSSH")); + compressors.addElement(new CompressorEntry("none", "")); + } + + public static String[] getDefaultCompressorList() + { + String list[] = new String[compressors.size()]; + for (int i = 0; i < compressors.size(); i++) + { + CompressorEntry ce = compressors.elementAt(i); + list[i] = new String(ce.type); + } + return list; + } + + public static void checkCompressorList(String[] compressorCandidates) + { + for (int i = 0; i < compressorCandidates.length; i++) + getEntry(compressorCandidates[i]); + } + + public static ICompressor createCompressor(String type) + { + try + { + CompressorEntry ce = getEntry(type); + if ("".equals(ce.compressorClass)) + return null; + + Class cc = Class.forName(ce.compressorClass); + ICompressor cmp = (ICompressor) cc.newInstance(); + + return cmp; + } + catch (Exception e) + { + throw new IllegalArgumentException("Cannot instantiate " + type); + } + } + + private static CompressorEntry getEntry(String type) + { + for (int i = 0; i < compressors.size(); i++) + { + CompressorEntry ce = compressors.elementAt(i); + if (ce.type.equals(type)) + return ce; + } + throw new IllegalArgumentException("Unkown algorithm " + type); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/compression/ICompressor.java b/app/src/main/java/com/trilead/ssh2/compression/ICompressor.java new file mode 100644 index 0000000..0b435b9 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/compression/ICompressor.java @@ -0,0 +1,32 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2.compression; + +/** + * @author Kenny Root + * + */ +public interface ICompressor { + int getBufferSize(); + + int compress(byte[] buf, int start, int len, byte[] output); + + byte[] uncompress(byte[] buf, int start, int[] len); + + boolean canCompressPreauth(); +} diff --git a/app/src/main/java/com/trilead/ssh2/compression/Zlib.java b/app/src/main/java/com/trilead/ssh2/compression/Zlib.java new file mode 100644 index 0000000..c1203a3 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/compression/Zlib.java @@ -0,0 +1,130 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2.compression; + +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZStream; + +/** + * @author Kenny Root + * + */ +public class Zlib implements ICompressor { + static private final int DEFAULT_BUF_SIZE = 4096; + static private final int LEVEL = 5; + + private ZStream deflate; + private byte[] deflate_tmpbuf; + + private ZStream inflate; + private byte[] inflate_tmpbuf; + private byte[] inflated_buf; + + public Zlib() { + deflate = new ZStream(); + inflate = new ZStream(); + + deflate.deflateInit(LEVEL); + inflate.inflateInit(); + + deflate_tmpbuf = new byte[DEFAULT_BUF_SIZE]; + inflate_tmpbuf = new byte[DEFAULT_BUF_SIZE]; + inflated_buf = new byte[DEFAULT_BUF_SIZE]; + } + + public boolean canCompressPreauth() { + return true; + } + + public int getBufferSize() { + return DEFAULT_BUF_SIZE; + } + + public int compress(byte[] buf, int start, int len, byte[] output) { + deflate.next_in = buf; + deflate.next_in_index = start; + deflate.avail_in = len - start; + + if ((buf.length + 1024) > deflate_tmpbuf.length) { + deflate_tmpbuf = new byte[buf.length + 1024]; + } + + deflate.next_out = deflate_tmpbuf; + deflate.next_out_index = 0; + deflate.avail_out = output.length; + + if (deflate.deflate(JZlib.Z_PARTIAL_FLUSH) != JZlib.Z_OK) { + System.err.println("compress: compression failure"); + } + + if (deflate.avail_in > 0) { + System.err.println("compress: deflated data too large"); + } + + int outputlen = output.length - deflate.avail_out; + + System.arraycopy(deflate_tmpbuf, 0, output, 0, outputlen); + + return outputlen; + } + + public byte[] uncompress(byte[] buffer, int start, int[] length) { + int inflated_end = 0; + + inflate.next_in = buffer; + inflate.next_in_index = start; + inflate.avail_in = length[0]; + + while (true) { + inflate.next_out = inflate_tmpbuf; + inflate.next_out_index = 0; + inflate.avail_out = DEFAULT_BUF_SIZE; + int status = inflate.inflate(JZlib.Z_PARTIAL_FLUSH); + switch (status) { + case JZlib.Z_OK: + if (inflated_buf.length < inflated_end + DEFAULT_BUF_SIZE + - inflate.avail_out) { + byte[] foo = new byte[inflated_end + DEFAULT_BUF_SIZE + - inflate.avail_out]; + System.arraycopy(inflated_buf, 0, foo, 0, inflated_end); + inflated_buf = foo; + } + System.arraycopy(inflate_tmpbuf, 0, inflated_buf, inflated_end, + DEFAULT_BUF_SIZE - inflate.avail_out); + inflated_end += (DEFAULT_BUF_SIZE - inflate.avail_out); + length[0] = inflated_end; + break; + case JZlib.Z_BUF_ERROR: + if (inflated_end > buffer.length - start) { + byte[] foo = new byte[inflated_end + start]; + System.arraycopy(buffer, 0, foo, 0, start); + System.arraycopy(inflated_buf, 0, foo, start, inflated_end); + buffer = foo; + } else { + System.arraycopy(inflated_buf, 0, buffer, start, + inflated_end); + } + length[0] = inflated_end; + return buffer; + default: + System.err.println("uncompress: inflate returnd " + status); + return null; + } + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java b/app/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java new file mode 100644 index 0000000..266fff9 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java @@ -0,0 +1,35 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.trilead.ssh2.compression; + +/** + * Defines how zlib@openssh.org compression works. + * See + * http://www.openssh.org/txt/draft-miller-secsh-compression-delayed-00.txt + * compression is disabled until userauth has occurred. + * + * @author Matt Johnston + * + */ +public class ZlibOpenSSH extends Zlib { + + public boolean canCompressPreauth() { + return false; + } + +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/Base64.java b/app/src/main/java/com/trilead/ssh2/crypto/Base64.java new file mode 100644 index 0000000..93770ac --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/Base64.java @@ -0,0 +1,148 @@ + +package com.trilead.ssh2.crypto; + +import java.io.CharArrayWriter; +import java.io.IOException; + +/** + * Basic Base64 Support. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Base64.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class Base64 +{ + static final char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + + public static char[] encode(byte[] content) + { + CharArrayWriter cw = new CharArrayWriter((4 * content.length) / 3); + + int idx = 0; + + int x = 0; + + for (int i = 0; i < content.length; i++) + { + if (idx == 0) + x = (content[i] & 0xff) << 16; + else if (idx == 1) + x = x | ((content[i] & 0xff) << 8); + else + x = x | (content[i] & 0xff); + + idx++; + + if (idx == 3) + { + cw.write(alphabet[x >> 18]); + cw.write(alphabet[(x >> 12) & 0x3f]); + cw.write(alphabet[(x >> 6) & 0x3f]); + cw.write(alphabet[x & 0x3f]); + + idx = 0; + } + } + + if (idx == 1) + { + cw.write(alphabet[x >> 18]); + cw.write(alphabet[(x >> 12) & 0x3f]); + cw.write('='); + cw.write('='); + } + + if (idx == 2) + { + cw.write(alphabet[x >> 18]); + cw.write(alphabet[(x >> 12) & 0x3f]); + cw.write(alphabet[(x >> 6) & 0x3f]); + cw.write('='); + } + + return cw.toCharArray(); + } + + public static byte[] decode(char[] message) throws IOException + { + byte buff[] = new byte[4]; + byte dest[] = new byte[message.length]; + + int bpos = 0; + int destpos = 0; + + for (int i = 0; i < message.length; i++) + { + int c = message[i]; + + if ((c == '\n') || (c == '\r') || (c == ' ') || (c == '\t')) + continue; + + if ((c >= 'A') && (c <= 'Z')) + { + buff[bpos++] = (byte) (c - 'A'); + } + else if ((c >= 'a') && (c <= 'z')) + { + buff[bpos++] = (byte) ((c - 'a') + 26); + } + else if ((c >= '0') && (c <= '9')) + { + buff[bpos++] = (byte) ((c - '0') + 52); + } + else if (c == '+') + { + buff[bpos++] = 62; + } + else if (c == '/') + { + buff[bpos++] = 63; + } + else if (c == '=') + { + buff[bpos++] = 64; + } + else + { + throw new IOException("Illegal char in base64 code."); + } + + if (bpos == 4) + { + bpos = 0; + + if (buff[0] == 64) + break; + + if (buff[1] == 64) + throw new IOException("Unexpected '=' in base64 code."); + + if (buff[2] == 64) + { + int v = (((buff[0] & 0x3f) << 6) | ((buff[1] & 0x3f))); + dest[destpos++] = (byte) (v >> 4); + break; + } + else if (buff[3] == 64) + { + int v = (((buff[0] & 0x3f) << 12) | ((buff[1] & 0x3f) << 6) | ((buff[2] & 0x3f))); + dest[destpos++] = (byte) (v >> 10); + dest[destpos++] = (byte) (v >> 2); + break; + } + else + { + int v = (((buff[0] & 0x3f) << 18) | ((buff[1] & 0x3f) << 12) | ((buff[2] & 0x3f) << 6) | ((buff[3] & 0x3f))); + dest[destpos++] = (byte) (v >> 16); + dest[destpos++] = (byte) (v >> 8); + dest[destpos++] = (byte) (v); + } + } + } + + byte[] res = new byte[destpos]; + System.arraycopy(dest, 0, res, 0, destpos); + + return res; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java b/app/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java new file mode 100644 index 0000000..86959e7 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java @@ -0,0 +1,26 @@ + +package com.trilead.ssh2.crypto; + +import com.trilead.ssh2.compression.CompressionFactory; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.transport.KexManager; + + +/** + * CryptoWishList. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CryptoWishList.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class CryptoWishList +{ + public String[] kexAlgorithms = KexManager.getDefaultKexAlgorithmList(); + public String[] serverHostKeyAlgorithms = KexManager.getDefaultServerHostkeyAlgorithmList(); + public String[] c2s_enc_algos = BlockCipherFactory.getDefaultCipherList(); + public String[] s2c_enc_algos = BlockCipherFactory.getDefaultCipherList(); + public String[] c2s_mac_algos = MAC.getMacList(); + public String[] s2c_mac_algos = MAC.getMacList(); + public String[] c2s_comp_algos = CompressionFactory.getDefaultCompressorList(); + public String[] s2c_comp_algos = CompressionFactory.getDefaultCompressorList(); +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java b/app/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java new file mode 100644 index 0000000..1dbd6c7 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java @@ -0,0 +1,91 @@ + +package com.trilead.ssh2.crypto; + + +import java.math.BigInteger; + +import com.trilead.ssh2.crypto.digest.HashForSSH2Types; + +/** + * Establishes key material for iv/key/mac (both directions). + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KeyMaterial.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KeyMaterial +{ + public byte[] initial_iv_client_to_server; + public byte[] initial_iv_server_to_client; + public byte[] enc_key_client_to_server; + public byte[] enc_key_server_to_client; + public byte[] integrity_key_client_to_server; + public byte[] integrity_key_server_to_client; + + private static byte[] calculateKey(HashForSSH2Types sh, BigInteger K, byte[] H, byte type, byte[] SessionID, + int keyLength) + { + byte[] res = new byte[keyLength]; + + int dglen = sh.getDigestLength(); + int numRounds = (keyLength + dglen - 1) / dglen; + + byte[][] tmp = new byte[numRounds][]; + + sh.reset(); + sh.updateBigInt(K); + sh.updateBytes(H); + sh.updateByte(type); + sh.updateBytes(SessionID); + + tmp[0] = sh.getDigest(); + + int off = 0; + int produced = Math.min(dglen, keyLength); + + System.arraycopy(tmp[0], 0, res, off, produced); + + keyLength -= produced; + off += produced; + + for (int i = 1; i < numRounds; i++) + { + sh.updateBigInt(K); + sh.updateBytes(H); + + for (int j = 0; j < i; j++) + sh.updateBytes(tmp[j]); + + tmp[i] = sh.getDigest(); + + produced = Math.min(dglen, keyLength); + System.arraycopy(tmp[i], 0, res, off, produced); + keyLength -= produced; + off += produced; + } + + return res; + } + + public static KeyMaterial create(String hashAlgo, byte[] H, BigInteger K, byte[] SessionID, int keyLengthCS, + int blockSizeCS, int macLengthCS, int keyLengthSC, int blockSizeSC, int macLengthSC) + throws IllegalArgumentException + { + KeyMaterial km = new KeyMaterial(); + + HashForSSH2Types sh = new HashForSSH2Types(hashAlgo); + + km.initial_iv_client_to_server = calculateKey(sh, K, H, (byte) 'A', SessionID, blockSizeCS); + + km.initial_iv_server_to_client = calculateKey(sh, K, H, (byte) 'B', SessionID, blockSizeSC); + + km.enc_key_client_to_server = calculateKey(sh, K, H, (byte) 'C', SessionID, keyLengthCS); + + km.enc_key_server_to_client = calculateKey(sh, K, H, (byte) 'D', SessionID, keyLengthSC); + + km.integrity_key_client_to_server = calculateKey(sh, K, H, (byte) 'E', SessionID, macLengthCS); + + km.integrity_key_server_to_client = calculateKey(sh, K, H, (byte) 'F', SessionID, macLengthSC); + + return km; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java b/app/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java new file mode 100644 index 0000000..5c0c2fd --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java @@ -0,0 +1,494 @@ + +package com.trilead.ssh2.crypto; + +import java.io.BufferedReader; +import java.io.CharArrayReader; +import java.io.IOException; +import java.math.BigInteger; +import java.security.DigestException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; + +import com.trilead.ssh2.crypto.cipher.AES; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.CBCMode; +import com.trilead.ssh2.crypto.cipher.DES; +import com.trilead.ssh2.crypto.cipher.DESede; +import com.trilead.ssh2.signature.ECDSASHA2Verify; + +/** + * PEM Support. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PEMDecoder.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class PEMDecoder +{ + public static final int PEM_RSA_PRIVATE_KEY = 1; + public static final int PEM_DSA_PRIVATE_KEY = 2; + public static final int PEM_EC_PRIVATE_KEY = 3; + + private static final int hexToInt(char c) + { + if ((c >= 'a') && (c <= 'f')) + { + return (c - 'a') + 10; + } + + if ((c >= 'A') && (c <= 'F')) + { + return (c - 'A') + 10; + } + + if ((c >= '0') && (c <= '9')) + { + return (c - '0'); + } + + throw new IllegalArgumentException("Need hex char"); + } + + private static byte[] hexToByteArray(String hex) + { + if (hex == null) + throw new IllegalArgumentException("null argument"); + + if ((hex.length() % 2) != 0) + throw new IllegalArgumentException("Uneven string length in hex encoding."); + + byte decoded[] = new byte[hex.length() / 2]; + + for (int i = 0; i < decoded.length; i++) + { + int hi = hexToInt(hex.charAt(i * 2)); + int lo = hexToInt(hex.charAt((i * 2) + 1)); + + decoded[i] = (byte) (hi * 16 + lo); + } + + return decoded; + } + + private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen) + throws IOException + { + if (salt.length < 8) + throw new IllegalArgumentException("Salt needs to be at least 8 bytes for key generation."); + + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("VM does not support MD5", e); + } + + byte[] key = new byte[keyLen]; + byte[] tmp = new byte[md5.getDigestLength()]; + + while (true) + { + md5.update(password, 0, password.length); + md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the + // salt in this step. + // This took me two hours until I got AES-xxx running. + + int copy = (keyLen < tmp.length) ? keyLen : tmp.length; + + try { + md5.digest(tmp, 0, tmp.length); + } catch (DigestException e) { + IOException ex = new IOException("could not digest password"); + ex.initCause(e); + throw ex; + } + + System.arraycopy(tmp, 0, key, key.length - keyLen, copy); + + keyLen -= copy; + + if (keyLen == 0) + return key; + + md5.update(tmp, 0, tmp.length); + } + } + + private static byte[] removePadding(byte[] buff, int blockSize) throws IOException + { + /* Removes RFC 1423/PKCS #7 padding */ + + int rfc_1423_padding = buff[buff.length - 1] & 0xff; + + if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize)) + throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); + + for (int i = 2; i <= rfc_1423_padding; i++) + { + if (buff[buff.length - i] != rfc_1423_padding) + throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); + } + + byte[] tmp = new byte[buff.length - rfc_1423_padding]; + System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding); + return tmp; + } + + public static final PEMStructure parsePEM(char[] pem) throws IOException + { + PEMStructure ps = new PEMStructure(); + + String line = null; + + BufferedReader br = new BufferedReader(new CharArrayReader(pem)); + + String endLine = null; + + while (true) + { + line = br.readLine(); + + if (line == null) + throw new IOException("Invalid PEM structure, '-----BEGIN...' missing"); + + line = line.trim(); + + if (line.startsWith("-----BEGIN DSA PRIVATE KEY-----")) + { + endLine = "-----END DSA PRIVATE KEY-----"; + ps.pemType = PEM_DSA_PRIVATE_KEY; + break; + } + + if (line.startsWith("-----BEGIN RSA PRIVATE KEY-----")) + { + endLine = "-----END RSA PRIVATE KEY-----"; + ps.pemType = PEM_RSA_PRIVATE_KEY; + break; + } + + if (line.startsWith("-----BEGIN EC PRIVATE KEY-----")) { + endLine = "-----END EC PRIVATE KEY-----"; + ps.pemType = PEM_EC_PRIVATE_KEY; + break; + } + } + + while (true) + { + line = br.readLine(); + + if (line == null) + throw new IOException("Invalid PEM structure, " + endLine + " missing"); + + line = line.trim(); + + int sem_idx = line.indexOf(':'); + + if (sem_idx == -1) + break; + + String name = line.substring(0, sem_idx + 1); + String value = line.substring(sem_idx + 1); + + String values[] = value.split(","); + + for (int i = 0; i < values.length; i++) + values[i] = values[i].trim(); + + // Proc-Type: 4,ENCRYPTED + // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483 + + if ("Proc-Type:".equals(name)) + { + ps.procType = values; + continue; + } + + if ("DEK-Info:".equals(name)) + { + ps.dekInfo = values; + continue; + } + /* Ignore line */ + } + + StringBuffer keyData = new StringBuffer(); + + while (true) + { + if (line == null) + throw new IOException("Invalid PEM structure, " + endLine + " missing"); + + line = line.trim(); + + if (line.startsWith(endLine)) + break; + + keyData.append(line); + + line = br.readLine(); + } + + char[] pem_chars = new char[keyData.length()]; + keyData.getChars(0, pem_chars.length, pem_chars, 0); + + ps.data = Base64.decode(pem_chars); + + if (ps.data.length == 0) + throw new IOException("Invalid PEM structure, no data available"); + + return ps; + } + + private static final void decryptPEM(PEMStructure ps, byte[] pw) throws IOException + { + if (ps.dekInfo == null) + throw new IOException("Broken PEM, no mode and salt given, but encryption enabled"); + + if (ps.dekInfo.length != 2) + throw new IOException("Broken PEM, DEK-Info is incomplete!"); + + String algo = ps.dekInfo[0]; + byte[] salt = hexToByteArray(ps.dekInfo[1]); + + BlockCipher bc = null; + + if (algo.equals("DES-EDE3-CBC")) + { + DESede des3 = new DESede(); + des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24)); + bc = new CBCMode(des3, salt, false); + } + else if (algo.equals("DES-CBC")) + { + DES des = new DES(); + des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8)); + bc = new CBCMode(des, salt, false); + } + else if (algo.equals("AES-128-CBC")) + { + AES aes = new AES(); + aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 16)); + bc = new CBCMode(aes, salt, false); + } + else if (algo.equals("AES-192-CBC")) + { + AES aes = new AES(); + aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24)); + bc = new CBCMode(aes, salt, false); + } + else if (algo.equals("AES-256-CBC")) + { + AES aes = new AES(); + aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32)); + bc = new CBCMode(aes, salt, false); + } + else + { + throw new IOException("Cannot decrypt PEM structure, unknown cipher " + algo); + } + + if ((ps.data.length % bc.getBlockSize()) != 0) + throw new IOException("Invalid PEM structure, size of encrypted block is not a multiple of " + + bc.getBlockSize()); + + /* Now decrypt the content */ + + byte[] dz = new byte[ps.data.length]; + + for (int i = 0; i < ps.data.length / bc.getBlockSize(); i++) + { + bc.transformBlock(ps.data, i * bc.getBlockSize(), dz, i * bc.getBlockSize()); + } + + /* Now check and remove RFC 1423/PKCS #7 padding */ + + dz = removePadding(dz, bc.getBlockSize()); + + ps.data = dz; + ps.dekInfo = null; + ps.procType = null; + } + + public static final boolean isPEMEncrypted(PEMStructure ps) throws IOException + { + if (ps.procType == null) + return false; + + if (ps.procType.length != 2) + throw new IOException("Unknown Proc-Type field."); + + if ("4".equals(ps.procType[0]) == false) + throw new IOException("Unknown Proc-Type field (" + ps.procType[0] + ")"); + + if ("ENCRYPTED".equals(ps.procType[1])) + return true; + + return false; + } + + public static KeyPair decode(char[] pem, String password) throws IOException + { + PEMStructure ps = parsePEM(pem); + return decode(ps, password); + } + + public static KeyPair decode(PEMStructure ps, String password) throws IOException + { + if (isPEMEncrypted(ps)) + { + if (password == null) + throw new IOException("PEM is encrypted, but no password was specified"); + + decryptPEM(ps, password.getBytes("ISO-8859-1")); + } + + if (ps.pemType == PEM_DSA_PRIVATE_KEY) + { + SimpleDERReader dr = new SimpleDERReader(ps.data); + + byte[] seq = dr.readSequenceAsByteArray(); + + if (dr.available() != 0) + throw new IOException("Padding in DSA PRIVATE KEY DER stream."); + + dr.resetInput(seq); + + BigInteger version = dr.readInt(); + + if (version.compareTo(BigInteger.ZERO) != 0) + throw new IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream."); + + BigInteger p = dr.readInt(); + BigInteger q = dr.readInt(); + BigInteger g = dr.readInt(); + BigInteger y = dr.readInt(); + BigInteger x = dr.readInt(); + + if (dr.available() != 0) + throw new IOException("Padding in DSA PRIVATE KEY DER stream."); + + DSAPrivateKeySpec privSpec = new DSAPrivateKeySpec(x, p, q, g); + DSAPublicKeySpec pubSpec = new DSAPublicKeySpec(y, p, q, g); + + return generateKeyPair("DSA", privSpec, pubSpec); + } + + if (ps.pemType == PEM_RSA_PRIVATE_KEY) + { + SimpleDERReader dr = new SimpleDERReader(ps.data); + + byte[] seq = dr.readSequenceAsByteArray(); + + if (dr.available() != 0) + throw new IOException("Padding in RSA PRIVATE KEY DER stream."); + + dr.resetInput(seq); + + BigInteger version = dr.readInt(); + + if ((version.compareTo(BigInteger.ZERO) != 0) && (version.compareTo(BigInteger.ONE) != 0)) + throw new IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream."); + + BigInteger n = dr.readInt(); + BigInteger e = dr.readInt(); + BigInteger d = dr.readInt(); + // TODO: is this right? + BigInteger primeP = dr.readInt(); + BigInteger primeQ = dr.readInt(); + BigInteger expP = dr.readInt(); + BigInteger expQ = dr.readInt(); + BigInteger coeff = dr.readInt(); + + RSAPrivateKeySpec privSpec = new RSAPrivateCrtKeySpec(n, e, d, primeP, primeQ, expP, expQ, coeff); + RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(n, e); + + return generateKeyPair("RSA", privSpec, pubSpec); + } + + if (ps.pemType == PEM_EC_PRIVATE_KEY) { + SimpleDERReader dr = new SimpleDERReader(ps.data); + + byte[] seq = dr.readSequenceAsByteArray(); + + if (dr.available() != 0) + throw new IOException("Padding in EC PRIVATE KEY DER stream."); + + dr.resetInput(seq); + + BigInteger version = dr.readInt(); + + if ((version.compareTo(BigInteger.ONE) != 0)) + throw new IOException("Wrong version (" + version + ") in EC PRIVATE KEY DER stream."); + + byte[] privateBytes = dr.readOctetString(); + + String curveOid = null; + byte[] publicBytes = null; + while (dr.available() > 0) { + int type = dr.readConstructedType(); + SimpleDERReader cr = dr.readConstructed(); + switch (type) { + case 0: + curveOid = cr.readOid(); + break; + case 1: + publicBytes = cr.readOctetString(); + break; + } + } + + ECParameterSpec params = ECDSASHA2Verify.getCurveForOID(curveOid); + if (params == null) + throw new IOException("invalid OID"); + + BigInteger s = new BigInteger(privateBytes); + byte[] publicBytesSlice = new byte[publicBytes.length - 1]; + System.arraycopy(publicBytes, 1, publicBytesSlice, 0, publicBytesSlice.length); + ECPoint w = ECDSASHA2Verify.decodeECPoint(publicBytesSlice, params.getCurve()); + + ECPrivateKeySpec privSpec = new ECPrivateKeySpec(s, params); + ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, params); + + return generateKeyPair("EC", privSpec, pubSpec); + } + + throw new IOException("PEM problem: it is of unknown type"); + } + + /** + * Generate a {@code KeyPair} given an {@code algorithm} and {@code KeySpec}. + */ + private static KeyPair generateKeyPair(String algorithm, KeySpec privSpec, KeySpec pubSpec) + throws IOException { + try { + final KeyFactory kf = KeyFactory.getInstance(algorithm); + final PublicKey pubKey = kf.generatePublic(pubSpec); + final PrivateKey privKey = kf.generatePrivate(privSpec); + return new KeyPair(pubKey, privKey); + } catch (NoSuchAlgorithmException ex) { + IOException ioex = new IOException(); + ioex.initCause(ex); + throw ioex; + } catch (InvalidKeySpecException ex) { + IOException ioex = new IOException("invalid keyspec"); + ioex.initCause(ex); + throw ioex; + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java b/app/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java new file mode 100644 index 0000000..83fb799 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java @@ -0,0 +1,17 @@ + +package com.trilead.ssh2.crypto; + +/** + * Parsed PEM structure. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PEMStructure.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ + +public class PEMStructure +{ + public int pemType; + String dekInfo[]; + String procType[]; + public byte[] data; +} \ No newline at end of file diff --git a/app/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java b/app/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java new file mode 100644 index 0000000..ff8112a --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java @@ -0,0 +1,229 @@ +package com.trilead.ssh2.crypto; + +import java.io.IOException; + +import java.math.BigInteger; + +/** + * SimpleDERReader. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: SimpleDERReader.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class SimpleDERReader +{ + private static final int CONSTRUCTED = 0x20; + + byte[] buffer; + int pos; + int count; + + public SimpleDERReader(byte[] b) + { + resetInput(b); + } + + public SimpleDERReader(byte[] b, int off, int len) + { + resetInput(b, off, len); + } + + public void resetInput(byte[] b) + { + resetInput(b, 0, b.length); + } + + public void resetInput(byte[] b, int off, int len) + { + buffer = b; + pos = off; + count = len; + } + + private byte readByte() throws IOException + { + if (count <= 0) + throw new IOException("DER byte array: out of data"); + count--; + return buffer[pos++]; + } + + private byte[] readBytes(int len) throws IOException + { + if (len > count) + throw new IOException("DER byte array: out of data"); + + byte[] b = new byte[len]; + + System.arraycopy(buffer, pos, b, 0, len); + + pos += len; + count -= len; + + return b; + } + + public int available() + { + return count; + } + + private int readLength() throws IOException + { + int len = readByte() & 0xff; + + if ((len & 0x80) == 0) + return len; + + int remain = len & 0x7F; + + if (remain == 0) + return -1; + + len = 0; + + while (remain > 0) + { + len = len << 8; + len = len | (readByte() & 0xff); + remain--; + } + + return len; + } + + public int ignoreNextObject() throws IOException + { + int type = readByte() & 0xff; + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + readBytes(len); + + return type; + } + + public BigInteger readInt() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x02) + throw new IOException("Expected DER Integer, but found type " + type); + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + BigInteger bi = new BigInteger(b); + + return bi; + } + + public int readConstructedType() throws IOException { + int type = readByte() & 0xff; + + if ((type & CONSTRUCTED) != CONSTRUCTED) + throw new IOException("Expected constructed type, but was " + type); + + return type & 0x1f; + } + + public SimpleDERReader readConstructed() throws IOException + { + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + SimpleDERReader cr = new SimpleDERReader(buffer, pos, len); + + pos += len; + count -= len; + + return cr; + } + + public byte[] readSequenceAsByteArray() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x30) + throw new IOException("Expected DER Sequence, but found type " + type); + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + return b; + } + + public String readOid() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x06) + throw new IOException("Expected DER OID, but found type " + type); + + int len = readLength(); + + if ((len < 1) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + long value = 0; + + StringBuilder sb = new StringBuilder(64); + switch(b[0] / 40) { + case 0: + sb.append('0'); + break; + case 1: + sb.append('1'); + b[0] -= 40; + break; + default: + sb.append('2'); + b[0] -= 80; + break; + } + + for (int i = 0; i < len; i++) { + value = (value << 7) + (b[i] & 0x7F); + if ((b[i] & 0x80) == 0) { + sb.append('.'); + sb.append(value); + value = 0; + } + } + + return sb.toString(); + } + + public byte[] readOctetString() throws IOException + { + int type = readByte() & 0xff; + + if (type != 0x04 && type != 0x03) + throw new IOException("Expected DER Octetstring, but found type " + type); + + int len = readLength(); + + if ((len < 0) || len > available()) + throw new IOException("Illegal len in DER object (" + len + ")"); + + byte[] b = readBytes(len); + + return b; + } + +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java new file mode 100644 index 0000000..e89e4a6 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java @@ -0,0 +1,698 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + This file was shamelessly taken from the Bouncy Castle Crypto package. + Their licence file states the following: + + Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + (http://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + * An implementation of the AES (Rijndael), from FIPS-197. + *

+ * For further details see: http://csrc.nist.gov/encryption/aes/ + * . + * + * This implementation is based on optimizations from Dr. Brian Gladman's paper + * and C code at http://fp.gladman.plus.com/cryptography_technology/rijndael/ + * + * + * There are three levels of tradeoff of speed vs memory Because java has no + * preprocessor, they are written as three separate classes from which to choose + * + * The fastest uses 8Kbytes of static tables to precompute round calculations, 4 + * 256 word tables for encryption and 4 for decryption. + * + * The middle performance version uses only one 256 word table for each, for a + * total of 2Kbytes, adding 12 rotate operations per round to compute the values + * contained in the other tables from the contents of the first + * + * The slowest version uses no static tables at all and computes the values in + * each round + *

+ * This file contains the fast version with 8Kbytes of static tables for round + * precomputation + * + * @author See comments in the source file + * @version $Id: AES.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class AES implements BlockCipher +{ + // The S box + private static final byte[] S = { (byte) 99, (byte) 124, (byte) 119, (byte) 123, (byte) 242, (byte) 107, + (byte) 111, (byte) 197, (byte) 48, (byte) 1, (byte) 103, (byte) 43, (byte) 254, (byte) 215, (byte) 171, + (byte) 118, (byte) 202, (byte) 130, (byte) 201, (byte) 125, (byte) 250, (byte) 89, (byte) 71, (byte) 240, + (byte) 173, (byte) 212, (byte) 162, (byte) 175, (byte) 156, (byte) 164, (byte) 114, (byte) 192, (byte) 183, + (byte) 253, (byte) 147, (byte) 38, (byte) 54, (byte) 63, (byte) 247, (byte) 204, (byte) 52, (byte) 165, + (byte) 229, (byte) 241, (byte) 113, (byte) 216, (byte) 49, (byte) 21, (byte) 4, (byte) 199, (byte) 35, + (byte) 195, (byte) 24, (byte) 150, (byte) 5, (byte) 154, (byte) 7, (byte) 18, (byte) 128, (byte) 226, + (byte) 235, (byte) 39, (byte) 178, (byte) 117, (byte) 9, (byte) 131, (byte) 44, (byte) 26, (byte) 27, + (byte) 110, (byte) 90, (byte) 160, (byte) 82, (byte) 59, (byte) 214, (byte) 179, (byte) 41, (byte) 227, + (byte) 47, (byte) 132, (byte) 83, (byte) 209, (byte) 0, (byte) 237, (byte) 32, (byte) 252, (byte) 177, + (byte) 91, (byte) 106, (byte) 203, (byte) 190, (byte) 57, (byte) 74, (byte) 76, (byte) 88, (byte) 207, + (byte) 208, (byte) 239, (byte) 170, (byte) 251, (byte) 67, (byte) 77, (byte) 51, (byte) 133, (byte) 69, + (byte) 249, (byte) 2, (byte) 127, (byte) 80, (byte) 60, (byte) 159, (byte) 168, (byte) 81, (byte) 163, + (byte) 64, (byte) 143, (byte) 146, (byte) 157, (byte) 56, (byte) 245, (byte) 188, (byte) 182, (byte) 218, + (byte) 33, (byte) 16, (byte) 255, (byte) 243, (byte) 210, (byte) 205, (byte) 12, (byte) 19, (byte) 236, + (byte) 95, (byte) 151, (byte) 68, (byte) 23, (byte) 196, (byte) 167, (byte) 126, (byte) 61, (byte) 100, + (byte) 93, (byte) 25, (byte) 115, (byte) 96, (byte) 129, (byte) 79, (byte) 220, (byte) 34, (byte) 42, + (byte) 144, (byte) 136, (byte) 70, (byte) 238, (byte) 184, (byte) 20, (byte) 222, (byte) 94, (byte) 11, + (byte) 219, (byte) 224, (byte) 50, (byte) 58, (byte) 10, (byte) 73, (byte) 6, (byte) 36, (byte) 92, + (byte) 194, (byte) 211, (byte) 172, (byte) 98, (byte) 145, (byte) 149, (byte) 228, (byte) 121, (byte) 231, + (byte) 200, (byte) 55, (byte) 109, (byte) 141, (byte) 213, (byte) 78, (byte) 169, (byte) 108, (byte) 86, + (byte) 244, (byte) 234, (byte) 101, (byte) 122, (byte) 174, (byte) 8, (byte) 186, (byte) 120, (byte) 37, + (byte) 46, (byte) 28, (byte) 166, (byte) 180, (byte) 198, (byte) 232, (byte) 221, (byte) 116, (byte) 31, + (byte) 75, (byte) 189, (byte) 139, (byte) 138, (byte) 112, (byte) 62, (byte) 181, (byte) 102, (byte) 72, + (byte) 3, (byte) 246, (byte) 14, (byte) 97, (byte) 53, (byte) 87, (byte) 185, (byte) 134, (byte) 193, + (byte) 29, (byte) 158, (byte) 225, (byte) 248, (byte) 152, (byte) 17, (byte) 105, (byte) 217, (byte) 142, + (byte) 148, (byte) 155, (byte) 30, (byte) 135, (byte) 233, (byte) 206, (byte) 85, (byte) 40, (byte) 223, + (byte) 140, (byte) 161, (byte) 137, (byte) 13, (byte) 191, (byte) 230, (byte) 66, (byte) 104, (byte) 65, + (byte) 153, (byte) 45, (byte) 15, (byte) 176, (byte) 84, (byte) 187, (byte) 22, }; + + // The inverse S-box + private static final byte[] Si = { (byte) 82, (byte) 9, (byte) 106, (byte) 213, (byte) 48, (byte) 54, (byte) 165, + (byte) 56, (byte) 191, (byte) 64, (byte) 163, (byte) 158, (byte) 129, (byte) 243, (byte) 215, (byte) 251, + (byte) 124, (byte) 227, (byte) 57, (byte) 130, (byte) 155, (byte) 47, (byte) 255, (byte) 135, (byte) 52, + (byte) 142, (byte) 67, (byte) 68, (byte) 196, (byte) 222, (byte) 233, (byte) 203, (byte) 84, (byte) 123, + (byte) 148, (byte) 50, (byte) 166, (byte) 194, (byte) 35, (byte) 61, (byte) 238, (byte) 76, (byte) 149, + (byte) 11, (byte) 66, (byte) 250, (byte) 195, (byte) 78, (byte) 8, (byte) 46, (byte) 161, (byte) 102, + (byte) 40, (byte) 217, (byte) 36, (byte) 178, (byte) 118, (byte) 91, (byte) 162, (byte) 73, (byte) 109, + (byte) 139, (byte) 209, (byte) 37, (byte) 114, (byte) 248, (byte) 246, (byte) 100, (byte) 134, (byte) 104, + (byte) 152, (byte) 22, (byte) 212, (byte) 164, (byte) 92, (byte) 204, (byte) 93, (byte) 101, (byte) 182, + (byte) 146, (byte) 108, (byte) 112, (byte) 72, (byte) 80, (byte) 253, (byte) 237, (byte) 185, (byte) 218, + (byte) 94, (byte) 21, (byte) 70, (byte) 87, (byte) 167, (byte) 141, (byte) 157, (byte) 132, (byte) 144, + (byte) 216, (byte) 171, (byte) 0, (byte) 140, (byte) 188, (byte) 211, (byte) 10, (byte) 247, (byte) 228, + (byte) 88, (byte) 5, (byte) 184, (byte) 179, (byte) 69, (byte) 6, (byte) 208, (byte) 44, (byte) 30, + (byte) 143, (byte) 202, (byte) 63, (byte) 15, (byte) 2, (byte) 193, (byte) 175, (byte) 189, (byte) 3, + (byte) 1, (byte) 19, (byte) 138, (byte) 107, (byte) 58, (byte) 145, (byte) 17, (byte) 65, (byte) 79, + (byte) 103, (byte) 220, (byte) 234, (byte) 151, (byte) 242, (byte) 207, (byte) 206, (byte) 240, (byte) 180, + (byte) 230, (byte) 115, (byte) 150, (byte) 172, (byte) 116, (byte) 34, (byte) 231, (byte) 173, (byte) 53, + (byte) 133, (byte) 226, (byte) 249, (byte) 55, (byte) 232, (byte) 28, (byte) 117, (byte) 223, (byte) 110, + (byte) 71, (byte) 241, (byte) 26, (byte) 113, (byte) 29, (byte) 41, (byte) 197, (byte) 137, (byte) 111, + (byte) 183, (byte) 98, (byte) 14, (byte) 170, (byte) 24, (byte) 190, (byte) 27, (byte) 252, (byte) 86, + (byte) 62, (byte) 75, (byte) 198, (byte) 210, (byte) 121, (byte) 32, (byte) 154, (byte) 219, (byte) 192, + (byte) 254, (byte) 120, (byte) 205, (byte) 90, (byte) 244, (byte) 31, (byte) 221, (byte) 168, (byte) 51, + (byte) 136, (byte) 7, (byte) 199, (byte) 49, (byte) 177, (byte) 18, (byte) 16, (byte) 89, (byte) 39, + (byte) 128, (byte) 236, (byte) 95, (byte) 96, (byte) 81, (byte) 127, (byte) 169, (byte) 25, (byte) 181, + (byte) 74, (byte) 13, (byte) 45, (byte) 229, (byte) 122, (byte) 159, (byte) 147, (byte) 201, (byte) 156, + (byte) 239, (byte) 160, (byte) 224, (byte) 59, (byte) 77, (byte) 174, (byte) 42, (byte) 245, (byte) 176, + (byte) 200, (byte) 235, (byte) 187, (byte) 60, (byte) 131, (byte) 83, (byte) 153, (byte) 97, (byte) 23, + (byte) 43, (byte) 4, (byte) 126, (byte) 186, (byte) 119, (byte) 214, (byte) 38, (byte) 225, (byte) 105, + (byte) 20, (byte) 99, (byte) 85, (byte) 33, (byte) 12, (byte) 125, }; + + // vector used in calculating key schedule (powers of x in GF(256)) + private static final int[] rcon = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, + 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 }; + + // precomputation tables of calculations for rounds + private static final int[] T0 = { 0xa56363c6, 0x847c7cf8, 0x997777ee, 0x8d7b7bf6, 0x0df2f2ff, 0xbd6b6bd6, + 0xb16f6fde, 0x54c5c591, 0x50303060, 0x03010102, 0xa96767ce, 0x7d2b2b56, 0x19fefee7, 0x62d7d7b5, 0xe6abab4d, + 0x9a7676ec, 0x45caca8f, 0x9d82821f, 0x40c9c989, 0x877d7dfa, 0x15fafaef, 0xeb5959b2, 0xc947478e, 0x0bf0f0fb, + 0xecadad41, 0x67d4d4b3, 0xfda2a25f, 0xeaafaf45, 0xbf9c9c23, 0xf7a4a453, 0x967272e4, 0x5bc0c09b, 0xc2b7b775, + 0x1cfdfde1, 0xae93933d, 0x6a26264c, 0x5a36366c, 0x413f3f7e, 0x02f7f7f5, 0x4fcccc83, 0x5c343468, 0xf4a5a551, + 0x34e5e5d1, 0x08f1f1f9, 0x937171e2, 0x73d8d8ab, 0x53313162, 0x3f15152a, 0x0c040408, 0x52c7c795, 0x65232346, + 0x5ec3c39d, 0x28181830, 0xa1969637, 0x0f05050a, 0xb59a9a2f, 0x0907070e, 0x36121224, 0x9b80801b, 0x3de2e2df, + 0x26ebebcd, 0x6927274e, 0xcdb2b27f, 0x9f7575ea, 0x1b090912, 0x9e83831d, 0x742c2c58, 0x2e1a1a34, 0x2d1b1b36, + 0xb26e6edc, 0xee5a5ab4, 0xfba0a05b, 0xf65252a4, 0x4d3b3b76, 0x61d6d6b7, 0xceb3b37d, 0x7b292952, 0x3ee3e3dd, + 0x712f2f5e, 0x97848413, 0xf55353a6, 0x68d1d1b9, 0x00000000, 0x2cededc1, 0x60202040, 0x1ffcfce3, 0xc8b1b179, + 0xed5b5bb6, 0xbe6a6ad4, 0x46cbcb8d, 0xd9bebe67, 0x4b393972, 0xde4a4a94, 0xd44c4c98, 0xe85858b0, 0x4acfcf85, + 0x6bd0d0bb, 0x2aefefc5, 0xe5aaaa4f, 0x16fbfbed, 0xc5434386, 0xd74d4d9a, 0x55333366, 0x94858511, 0xcf45458a, + 0x10f9f9e9, 0x06020204, 0x817f7ffe, 0xf05050a0, 0x443c3c78, 0xba9f9f25, 0xe3a8a84b, 0xf35151a2, 0xfea3a35d, + 0xc0404080, 0x8a8f8f05, 0xad92923f, 0xbc9d9d21, 0x48383870, 0x04f5f5f1, 0xdfbcbc63, 0xc1b6b677, 0x75dadaaf, + 0x63212142, 0x30101020, 0x1affffe5, 0x0ef3f3fd, 0x6dd2d2bf, 0x4ccdcd81, 0x140c0c18, 0x35131326, 0x2fececc3, + 0xe15f5fbe, 0xa2979735, 0xcc444488, 0x3917172e, 0x57c4c493, 0xf2a7a755, 0x827e7efc, 0x473d3d7a, 0xac6464c8, + 0xe75d5dba, 0x2b191932, 0x957373e6, 0xa06060c0, 0x98818119, 0xd14f4f9e, 0x7fdcdca3, 0x66222244, 0x7e2a2a54, + 0xab90903b, 0x8388880b, 0xca46468c, 0x29eeeec7, 0xd3b8b86b, 0x3c141428, 0x79dedea7, 0xe25e5ebc, 0x1d0b0b16, + 0x76dbdbad, 0x3be0e0db, 0x56323264, 0x4e3a3a74, 0x1e0a0a14, 0xdb494992, 0x0a06060c, 0x6c242448, 0xe45c5cb8, + 0x5dc2c29f, 0x6ed3d3bd, 0xefacac43, 0xa66262c4, 0xa8919139, 0xa4959531, 0x37e4e4d3, 0x8b7979f2, 0x32e7e7d5, + 0x43c8c88b, 0x5937376e, 0xb76d6dda, 0x8c8d8d01, 0x64d5d5b1, 0xd24e4e9c, 0xe0a9a949, 0xb46c6cd8, 0xfa5656ac, + 0x07f4f4f3, 0x25eaeacf, 0xaf6565ca, 0x8e7a7af4, 0xe9aeae47, 0x18080810, 0xd5baba6f, 0x887878f0, 0x6f25254a, + 0x722e2e5c, 0x241c1c38, 0xf1a6a657, 0xc7b4b473, 0x51c6c697, 0x23e8e8cb, 0x7cdddda1, 0x9c7474e8, 0x211f1f3e, + 0xdd4b4b96, 0xdcbdbd61, 0x868b8b0d, 0x858a8a0f, 0x907070e0, 0x423e3e7c, 0xc4b5b571, 0xaa6666cc, 0xd8484890, + 0x05030306, 0x01f6f6f7, 0x120e0e1c, 0xa36161c2, 0x5f35356a, 0xf95757ae, 0xd0b9b969, 0x91868617, 0x58c1c199, + 0x271d1d3a, 0xb99e9e27, 0x38e1e1d9, 0x13f8f8eb, 0xb398982b, 0x33111122, 0xbb6969d2, 0x70d9d9a9, 0x898e8e07, + 0xa7949433, 0xb69b9b2d, 0x221e1e3c, 0x92878715, 0x20e9e9c9, 0x49cece87, 0xff5555aa, 0x78282850, 0x7adfdfa5, + 0x8f8c8c03, 0xf8a1a159, 0x80898909, 0x170d0d1a, 0xdabfbf65, 0x31e6e6d7, 0xc6424284, 0xb86868d0, 0xc3414182, + 0xb0999929, 0x772d2d5a, 0x110f0f1e, 0xcbb0b07b, 0xfc5454a8, 0xd6bbbb6d, 0x3a16162c }; + + private static final int[] T1 = { 0x6363c6a5, 0x7c7cf884, 0x7777ee99, 0x7b7bf68d, 0xf2f2ff0d, 0x6b6bd6bd, + 0x6f6fdeb1, 0xc5c59154, 0x30306050, 0x01010203, 0x6767cea9, 0x2b2b567d, 0xfefee719, 0xd7d7b562, 0xabab4de6, + 0x7676ec9a, 0xcaca8f45, 0x82821f9d, 0xc9c98940, 0x7d7dfa87, 0xfafaef15, 0x5959b2eb, 0x47478ec9, 0xf0f0fb0b, + 0xadad41ec, 0xd4d4b367, 0xa2a25ffd, 0xafaf45ea, 0x9c9c23bf, 0xa4a453f7, 0x7272e496, 0xc0c09b5b, 0xb7b775c2, + 0xfdfde11c, 0x93933dae, 0x26264c6a, 0x36366c5a, 0x3f3f7e41, 0xf7f7f502, 0xcccc834f, 0x3434685c, 0xa5a551f4, + 0xe5e5d134, 0xf1f1f908, 0x7171e293, 0xd8d8ab73, 0x31316253, 0x15152a3f, 0x0404080c, 0xc7c79552, 0x23234665, + 0xc3c39d5e, 0x18183028, 0x969637a1, 0x05050a0f, 0x9a9a2fb5, 0x07070e09, 0x12122436, 0x80801b9b, 0xe2e2df3d, + 0xebebcd26, 0x27274e69, 0xb2b27fcd, 0x7575ea9f, 0x0909121b, 0x83831d9e, 0x2c2c5874, 0x1a1a342e, 0x1b1b362d, + 0x6e6edcb2, 0x5a5ab4ee, 0xa0a05bfb, 0x5252a4f6, 0x3b3b764d, 0xd6d6b761, 0xb3b37dce, 0x2929527b, 0xe3e3dd3e, + 0x2f2f5e71, 0x84841397, 0x5353a6f5, 0xd1d1b968, 0x00000000, 0xededc12c, 0x20204060, 0xfcfce31f, 0xb1b179c8, + 0x5b5bb6ed, 0x6a6ad4be, 0xcbcb8d46, 0xbebe67d9, 0x3939724b, 0x4a4a94de, 0x4c4c98d4, 0x5858b0e8, 0xcfcf854a, + 0xd0d0bb6b, 0xefefc52a, 0xaaaa4fe5, 0xfbfbed16, 0x434386c5, 0x4d4d9ad7, 0x33336655, 0x85851194, 0x45458acf, + 0xf9f9e910, 0x02020406, 0x7f7ffe81, 0x5050a0f0, 0x3c3c7844, 0x9f9f25ba, 0xa8a84be3, 0x5151a2f3, 0xa3a35dfe, + 0x404080c0, 0x8f8f058a, 0x92923fad, 0x9d9d21bc, 0x38387048, 0xf5f5f104, 0xbcbc63df, 0xb6b677c1, 0xdadaaf75, + 0x21214263, 0x10102030, 0xffffe51a, 0xf3f3fd0e, 0xd2d2bf6d, 0xcdcd814c, 0x0c0c1814, 0x13132635, 0xececc32f, + 0x5f5fbee1, 0x979735a2, 0x444488cc, 0x17172e39, 0xc4c49357, 0xa7a755f2, 0x7e7efc82, 0x3d3d7a47, 0x6464c8ac, + 0x5d5dbae7, 0x1919322b, 0x7373e695, 0x6060c0a0, 0x81811998, 0x4f4f9ed1, 0xdcdca37f, 0x22224466, 0x2a2a547e, + 0x90903bab, 0x88880b83, 0x46468cca, 0xeeeec729, 0xb8b86bd3, 0x1414283c, 0xdedea779, 0x5e5ebce2, 0x0b0b161d, + 0xdbdbad76, 0xe0e0db3b, 0x32326456, 0x3a3a744e, 0x0a0a141e, 0x494992db, 0x06060c0a, 0x2424486c, 0x5c5cb8e4, + 0xc2c29f5d, 0xd3d3bd6e, 0xacac43ef, 0x6262c4a6, 0x919139a8, 0x959531a4, 0xe4e4d337, 0x7979f28b, 0xe7e7d532, + 0xc8c88b43, 0x37376e59, 0x6d6ddab7, 0x8d8d018c, 0xd5d5b164, 0x4e4e9cd2, 0xa9a949e0, 0x6c6cd8b4, 0x5656acfa, + 0xf4f4f307, 0xeaeacf25, 0x6565caaf, 0x7a7af48e, 0xaeae47e9, 0x08081018, 0xbaba6fd5, 0x7878f088, 0x25254a6f, + 0x2e2e5c72, 0x1c1c3824, 0xa6a657f1, 0xb4b473c7, 0xc6c69751, 0xe8e8cb23, 0xdddda17c, 0x7474e89c, 0x1f1f3e21, + 0x4b4b96dd, 0xbdbd61dc, 0x8b8b0d86, 0x8a8a0f85, 0x7070e090, 0x3e3e7c42, 0xb5b571c4, 0x6666ccaa, 0x484890d8, + 0x03030605, 0xf6f6f701, 0x0e0e1c12, 0x6161c2a3, 0x35356a5f, 0x5757aef9, 0xb9b969d0, 0x86861791, 0xc1c19958, + 0x1d1d3a27, 0x9e9e27b9, 0xe1e1d938, 0xf8f8eb13, 0x98982bb3, 0x11112233, 0x6969d2bb, 0xd9d9a970, 0x8e8e0789, + 0x949433a7, 0x9b9b2db6, 0x1e1e3c22, 0x87871592, 0xe9e9c920, 0xcece8749, 0x5555aaff, 0x28285078, 0xdfdfa57a, + 0x8c8c038f, 0xa1a159f8, 0x89890980, 0x0d0d1a17, 0xbfbf65da, 0xe6e6d731, 0x424284c6, 0x6868d0b8, 0x414182c3, + 0x999929b0, 0x2d2d5a77, 0x0f0f1e11, 0xb0b07bcb, 0x5454a8fc, 0xbbbb6dd6, 0x16162c3a }; + + private static final int[] T2 = { 0x63c6a563, 0x7cf8847c, 0x77ee9977, 0x7bf68d7b, 0xf2ff0df2, 0x6bd6bd6b, + 0x6fdeb16f, 0xc59154c5, 0x30605030, 0x01020301, 0x67cea967, 0x2b567d2b, 0xfee719fe, 0xd7b562d7, 0xab4de6ab, + 0x76ec9a76, 0xca8f45ca, 0x821f9d82, 0xc98940c9, 0x7dfa877d, 0xfaef15fa, 0x59b2eb59, 0x478ec947, 0xf0fb0bf0, + 0xad41ecad, 0xd4b367d4, 0xa25ffda2, 0xaf45eaaf, 0x9c23bf9c, 0xa453f7a4, 0x72e49672, 0xc09b5bc0, 0xb775c2b7, + 0xfde11cfd, 0x933dae93, 0x264c6a26, 0x366c5a36, 0x3f7e413f, 0xf7f502f7, 0xcc834fcc, 0x34685c34, 0xa551f4a5, + 0xe5d134e5, 0xf1f908f1, 0x71e29371, 0xd8ab73d8, 0x31625331, 0x152a3f15, 0x04080c04, 0xc79552c7, 0x23466523, + 0xc39d5ec3, 0x18302818, 0x9637a196, 0x050a0f05, 0x9a2fb59a, 0x070e0907, 0x12243612, 0x801b9b80, 0xe2df3de2, + 0xebcd26eb, 0x274e6927, 0xb27fcdb2, 0x75ea9f75, 0x09121b09, 0x831d9e83, 0x2c58742c, 0x1a342e1a, 0x1b362d1b, + 0x6edcb26e, 0x5ab4ee5a, 0xa05bfba0, 0x52a4f652, 0x3b764d3b, 0xd6b761d6, 0xb37dceb3, 0x29527b29, 0xe3dd3ee3, + 0x2f5e712f, 0x84139784, 0x53a6f553, 0xd1b968d1, 0x00000000, 0xedc12ced, 0x20406020, 0xfce31ffc, 0xb179c8b1, + 0x5bb6ed5b, 0x6ad4be6a, 0xcb8d46cb, 0xbe67d9be, 0x39724b39, 0x4a94de4a, 0x4c98d44c, 0x58b0e858, 0xcf854acf, + 0xd0bb6bd0, 0xefc52aef, 0xaa4fe5aa, 0xfbed16fb, 0x4386c543, 0x4d9ad74d, 0x33665533, 0x85119485, 0x458acf45, + 0xf9e910f9, 0x02040602, 0x7ffe817f, 0x50a0f050, 0x3c78443c, 0x9f25ba9f, 0xa84be3a8, 0x51a2f351, 0xa35dfea3, + 0x4080c040, 0x8f058a8f, 0x923fad92, 0x9d21bc9d, 0x38704838, 0xf5f104f5, 0xbc63dfbc, 0xb677c1b6, 0xdaaf75da, + 0x21426321, 0x10203010, 0xffe51aff, 0xf3fd0ef3, 0xd2bf6dd2, 0xcd814ccd, 0x0c18140c, 0x13263513, 0xecc32fec, + 0x5fbee15f, 0x9735a297, 0x4488cc44, 0x172e3917, 0xc49357c4, 0xa755f2a7, 0x7efc827e, 0x3d7a473d, 0x64c8ac64, + 0x5dbae75d, 0x19322b19, 0x73e69573, 0x60c0a060, 0x81199881, 0x4f9ed14f, 0xdca37fdc, 0x22446622, 0x2a547e2a, + 0x903bab90, 0x880b8388, 0x468cca46, 0xeec729ee, 0xb86bd3b8, 0x14283c14, 0xdea779de, 0x5ebce25e, 0x0b161d0b, + 0xdbad76db, 0xe0db3be0, 0x32645632, 0x3a744e3a, 0x0a141e0a, 0x4992db49, 0x060c0a06, 0x24486c24, 0x5cb8e45c, + 0xc29f5dc2, 0xd3bd6ed3, 0xac43efac, 0x62c4a662, 0x9139a891, 0x9531a495, 0xe4d337e4, 0x79f28b79, 0xe7d532e7, + 0xc88b43c8, 0x376e5937, 0x6ddab76d, 0x8d018c8d, 0xd5b164d5, 0x4e9cd24e, 0xa949e0a9, 0x6cd8b46c, 0x56acfa56, + 0xf4f307f4, 0xeacf25ea, 0x65caaf65, 0x7af48e7a, 0xae47e9ae, 0x08101808, 0xba6fd5ba, 0x78f08878, 0x254a6f25, + 0x2e5c722e, 0x1c38241c, 0xa657f1a6, 0xb473c7b4, 0xc69751c6, 0xe8cb23e8, 0xdda17cdd, 0x74e89c74, 0x1f3e211f, + 0x4b96dd4b, 0xbd61dcbd, 0x8b0d868b, 0x8a0f858a, 0x70e09070, 0x3e7c423e, 0xb571c4b5, 0x66ccaa66, 0x4890d848, + 0x03060503, 0xf6f701f6, 0x0e1c120e, 0x61c2a361, 0x356a5f35, 0x57aef957, 0xb969d0b9, 0x86179186, 0xc19958c1, + 0x1d3a271d, 0x9e27b99e, 0xe1d938e1, 0xf8eb13f8, 0x982bb398, 0x11223311, 0x69d2bb69, 0xd9a970d9, 0x8e07898e, + 0x9433a794, 0x9b2db69b, 0x1e3c221e, 0x87159287, 0xe9c920e9, 0xce8749ce, 0x55aaff55, 0x28507828, 0xdfa57adf, + 0x8c038f8c, 0xa159f8a1, 0x89098089, 0x0d1a170d, 0xbf65dabf, 0xe6d731e6, 0x4284c642, 0x68d0b868, 0x4182c341, + 0x9929b099, 0x2d5a772d, 0x0f1e110f, 0xb07bcbb0, 0x54a8fc54, 0xbb6dd6bb, 0x162c3a16 }; + + private static final int[] T3 = { 0xc6a56363, 0xf8847c7c, 0xee997777, 0xf68d7b7b, 0xff0df2f2, 0xd6bd6b6b, + 0xdeb16f6f, 0x9154c5c5, 0x60503030, 0x02030101, 0xcea96767, 0x567d2b2b, 0xe719fefe, 0xb562d7d7, 0x4de6abab, + 0xec9a7676, 0x8f45caca, 0x1f9d8282, 0x8940c9c9, 0xfa877d7d, 0xef15fafa, 0xb2eb5959, 0x8ec94747, 0xfb0bf0f0, + 0x41ecadad, 0xb367d4d4, 0x5ffda2a2, 0x45eaafaf, 0x23bf9c9c, 0x53f7a4a4, 0xe4967272, 0x9b5bc0c0, 0x75c2b7b7, + 0xe11cfdfd, 0x3dae9393, 0x4c6a2626, 0x6c5a3636, 0x7e413f3f, 0xf502f7f7, 0x834fcccc, 0x685c3434, 0x51f4a5a5, + 0xd134e5e5, 0xf908f1f1, 0xe2937171, 0xab73d8d8, 0x62533131, 0x2a3f1515, 0x080c0404, 0x9552c7c7, 0x46652323, + 0x9d5ec3c3, 0x30281818, 0x37a19696, 0x0a0f0505, 0x2fb59a9a, 0x0e090707, 0x24361212, 0x1b9b8080, 0xdf3de2e2, + 0xcd26ebeb, 0x4e692727, 0x7fcdb2b2, 0xea9f7575, 0x121b0909, 0x1d9e8383, 0x58742c2c, 0x342e1a1a, 0x362d1b1b, + 0xdcb26e6e, 0xb4ee5a5a, 0x5bfba0a0, 0xa4f65252, 0x764d3b3b, 0xb761d6d6, 0x7dceb3b3, 0x527b2929, 0xdd3ee3e3, + 0x5e712f2f, 0x13978484, 0xa6f55353, 0xb968d1d1, 0x00000000, 0xc12ceded, 0x40602020, 0xe31ffcfc, 0x79c8b1b1, + 0xb6ed5b5b, 0xd4be6a6a, 0x8d46cbcb, 0x67d9bebe, 0x724b3939, 0x94de4a4a, 0x98d44c4c, 0xb0e85858, 0x854acfcf, + 0xbb6bd0d0, 0xc52aefef, 0x4fe5aaaa, 0xed16fbfb, 0x86c54343, 0x9ad74d4d, 0x66553333, 0x11948585, 0x8acf4545, + 0xe910f9f9, 0x04060202, 0xfe817f7f, 0xa0f05050, 0x78443c3c, 0x25ba9f9f, 0x4be3a8a8, 0xa2f35151, 0x5dfea3a3, + 0x80c04040, 0x058a8f8f, 0x3fad9292, 0x21bc9d9d, 0x70483838, 0xf104f5f5, 0x63dfbcbc, 0x77c1b6b6, 0xaf75dada, + 0x42632121, 0x20301010, 0xe51affff, 0xfd0ef3f3, 0xbf6dd2d2, 0x814ccdcd, 0x18140c0c, 0x26351313, 0xc32fecec, + 0xbee15f5f, 0x35a29797, 0x88cc4444, 0x2e391717, 0x9357c4c4, 0x55f2a7a7, 0xfc827e7e, 0x7a473d3d, 0xc8ac6464, + 0xbae75d5d, 0x322b1919, 0xe6957373, 0xc0a06060, 0x19988181, 0x9ed14f4f, 0xa37fdcdc, 0x44662222, 0x547e2a2a, + 0x3bab9090, 0x0b838888, 0x8cca4646, 0xc729eeee, 0x6bd3b8b8, 0x283c1414, 0xa779dede, 0xbce25e5e, 0x161d0b0b, + 0xad76dbdb, 0xdb3be0e0, 0x64563232, 0x744e3a3a, 0x141e0a0a, 0x92db4949, 0x0c0a0606, 0x486c2424, 0xb8e45c5c, + 0x9f5dc2c2, 0xbd6ed3d3, 0x43efacac, 0xc4a66262, 0x39a89191, 0x31a49595, 0xd337e4e4, 0xf28b7979, 0xd532e7e7, + 0x8b43c8c8, 0x6e593737, 0xdab76d6d, 0x018c8d8d, 0xb164d5d5, 0x9cd24e4e, 0x49e0a9a9, 0xd8b46c6c, 0xacfa5656, + 0xf307f4f4, 0xcf25eaea, 0xcaaf6565, 0xf48e7a7a, 0x47e9aeae, 0x10180808, 0x6fd5baba, 0xf0887878, 0x4a6f2525, + 0x5c722e2e, 0x38241c1c, 0x57f1a6a6, 0x73c7b4b4, 0x9751c6c6, 0xcb23e8e8, 0xa17cdddd, 0xe89c7474, 0x3e211f1f, + 0x96dd4b4b, 0x61dcbdbd, 0x0d868b8b, 0x0f858a8a, 0xe0907070, 0x7c423e3e, 0x71c4b5b5, 0xccaa6666, 0x90d84848, + 0x06050303, 0xf701f6f6, 0x1c120e0e, 0xc2a36161, 0x6a5f3535, 0xaef95757, 0x69d0b9b9, 0x17918686, 0x9958c1c1, + 0x3a271d1d, 0x27b99e9e, 0xd938e1e1, 0xeb13f8f8, 0x2bb39898, 0x22331111, 0xd2bb6969, 0xa970d9d9, 0x07898e8e, + 0x33a79494, 0x2db69b9b, 0x3c221e1e, 0x15928787, 0xc920e9e9, 0x8749cece, 0xaaff5555, 0x50782828, 0xa57adfdf, + 0x038f8c8c, 0x59f8a1a1, 0x09808989, 0x1a170d0d, 0x65dabfbf, 0xd731e6e6, 0x84c64242, 0xd0b86868, 0x82c34141, + 0x29b09999, 0x5a772d2d, 0x1e110f0f, 0x7bcbb0b0, 0xa8fc5454, 0x6dd6bbbb, 0x2c3a1616 }; + + private static final int[] Tinv0 = { 0x50a7f451, 0x5365417e, 0xc3a4171a, 0x965e273a, 0xcb6bab3b, 0xf1459d1f, + 0xab58faac, 0x9303e34b, 0x55fa3020, 0xf66d76ad, 0x9176cc88, 0x254c02f5, 0xfcd7e54f, 0xd7cb2ac5, 0x80443526, + 0x8fa362b5, 0x495ab1de, 0x671bba25, 0x980eea45, 0xe1c0fe5d, 0x02752fc3, 0x12f04c81, 0xa397468d, 0xc6f9d36b, + 0xe75f8f03, 0x959c9215, 0xeb7a6dbf, 0xda595295, 0x2d83bed4, 0xd3217458, 0x2969e049, 0x44c8c98e, 0x6a89c275, + 0x78798ef4, 0x6b3e5899, 0xdd71b927, 0xb64fe1be, 0x17ad88f0, 0x66ac20c9, 0xb43ace7d, 0x184adf63, 0x82311ae5, + 0x60335197, 0x457f5362, 0xe07764b1, 0x84ae6bbb, 0x1ca081fe, 0x942b08f9, 0x58684870, 0x19fd458f, 0x876cde94, + 0xb7f87b52, 0x23d373ab, 0xe2024b72, 0x578f1fe3, 0x2aab5566, 0x0728ebb2, 0x03c2b52f, 0x9a7bc586, 0xa50837d3, + 0xf2872830, 0xb2a5bf23, 0xba6a0302, 0x5c8216ed, 0x2b1ccf8a, 0x92b479a7, 0xf0f207f3, 0xa1e2694e, 0xcdf4da65, + 0xd5be0506, 0x1f6234d1, 0x8afea6c4, 0x9d532e34, 0xa055f3a2, 0x32e18a05, 0x75ebf6a4, 0x39ec830b, 0xaaef6040, + 0x069f715e, 0x51106ebd, 0xf98a213e, 0x3d06dd96, 0xae053edd, 0x46bde64d, 0xb58d5491, 0x055dc471, 0x6fd40604, + 0xff155060, 0x24fb9819, 0x97e9bdd6, 0xcc434089, 0x779ed967, 0xbd42e8b0, 0x888b8907, 0x385b19e7, 0xdbeec879, + 0x470a7ca1, 0xe90f427c, 0xc91e84f8, 0x00000000, 0x83868009, 0x48ed2b32, 0xac70111e, 0x4e725a6c, 0xfbff0efd, + 0x5638850f, 0x1ed5ae3d, 0x27392d36, 0x64d90f0a, 0x21a65c68, 0xd1545b9b, 0x3a2e3624, 0xb1670a0c, 0x0fe75793, + 0xd296eeb4, 0x9e919b1b, 0x4fc5c080, 0xa220dc61, 0x694b775a, 0x161a121c, 0x0aba93e2, 0xe52aa0c0, 0x43e0223c, + 0x1d171b12, 0x0b0d090e, 0xadc78bf2, 0xb9a8b62d, 0xc8a91e14, 0x8519f157, 0x4c0775af, 0xbbdd99ee, 0xfd607fa3, + 0x9f2601f7, 0xbcf5725c, 0xc53b6644, 0x347efb5b, 0x7629438b, 0xdcc623cb, 0x68fcedb6, 0x63f1e4b8, 0xcadc31d7, + 0x10856342, 0x40229713, 0x2011c684, 0x7d244a85, 0xf83dbbd2, 0x1132f9ae, 0x6da129c7, 0x4b2f9e1d, 0xf330b2dc, + 0xec52860d, 0xd0e3c177, 0x6c16b32b, 0x99b970a9, 0xfa489411, 0x2264e947, 0xc48cfca8, 0x1a3ff0a0, 0xd82c7d56, + 0xef903322, 0xc74e4987, 0xc1d138d9, 0xfea2ca8c, 0x360bd498, 0xcf81f5a6, 0x28de7aa5, 0x268eb7da, 0xa4bfad3f, + 0xe49d3a2c, 0x0d927850, 0x9bcc5f6a, 0x62467e54, 0xc2138df6, 0xe8b8d890, 0x5ef7392e, 0xf5afc382, 0xbe805d9f, + 0x7c93d069, 0xa92dd56f, 0xb31225cf, 0x3b99acc8, 0xa77d1810, 0x6e639ce8, 0x7bbb3bdb, 0x097826cd, 0xf418596e, + 0x01b79aec, 0xa89a4f83, 0x656e95e6, 0x7ee6ffaa, 0x08cfbc21, 0xe6e815ef, 0xd99be7ba, 0xce366f4a, 0xd4099fea, + 0xd67cb029, 0xafb2a431, 0x31233f2a, 0x3094a5c6, 0xc066a235, 0x37bc4e74, 0xa6ca82fc, 0xb0d090e0, 0x15d8a733, + 0x4a9804f1, 0xf7daec41, 0x0e50cd7f, 0x2ff69117, 0x8dd64d76, 0x4db0ef43, 0x544daacc, 0xdf0496e4, 0xe3b5d19e, + 0x1b886a4c, 0xb81f2cc1, 0x7f516546, 0x04ea5e9d, 0x5d358c01, 0x737487fa, 0x2e410bfb, 0x5a1d67b3, 0x52d2db92, + 0x335610e9, 0x1347d66d, 0x8c61d79a, 0x7a0ca137, 0x8e14f859, 0x893c13eb, 0xee27a9ce, 0x35c961b7, 0xede51ce1, + 0x3cb1477a, 0x59dfd29c, 0x3f73f255, 0x79ce1418, 0xbf37c773, 0xeacdf753, 0x5baafd5f, 0x146f3ddf, 0x86db4478, + 0x81f3afca, 0x3ec468b9, 0x2c342438, 0x5f40a3c2, 0x72c31d16, 0x0c25e2bc, 0x8b493c28, 0x41950dff, 0x7101a839, + 0xdeb30c08, 0x9ce4b4d8, 0x90c15664, 0x6184cb7b, 0x70b632d5, 0x745c6c48, 0x4257b8d0 }; + + private static final int[] Tinv1 = { 0xa7f45150, 0x65417e53, 0xa4171ac3, 0x5e273a96, 0x6bab3bcb, 0x459d1ff1, + 0x58faacab, 0x03e34b93, 0xfa302055, 0x6d76adf6, 0x76cc8891, 0x4c02f525, 0xd7e54ffc, 0xcb2ac5d7, 0x44352680, + 0xa362b58f, 0x5ab1de49, 0x1bba2567, 0x0eea4598, 0xc0fe5de1, 0x752fc302, 0xf04c8112, 0x97468da3, 0xf9d36bc6, + 0x5f8f03e7, 0x9c921595, 0x7a6dbfeb, 0x595295da, 0x83bed42d, 0x217458d3, 0x69e04929, 0xc8c98e44, 0x89c2756a, + 0x798ef478, 0x3e58996b, 0x71b927dd, 0x4fe1beb6, 0xad88f017, 0xac20c966, 0x3ace7db4, 0x4adf6318, 0x311ae582, + 0x33519760, 0x7f536245, 0x7764b1e0, 0xae6bbb84, 0xa081fe1c, 0x2b08f994, 0x68487058, 0xfd458f19, 0x6cde9487, + 0xf87b52b7, 0xd373ab23, 0x024b72e2, 0x8f1fe357, 0xab55662a, 0x28ebb207, 0xc2b52f03, 0x7bc5869a, 0x0837d3a5, + 0x872830f2, 0xa5bf23b2, 0x6a0302ba, 0x8216ed5c, 0x1ccf8a2b, 0xb479a792, 0xf207f3f0, 0xe2694ea1, 0xf4da65cd, + 0xbe0506d5, 0x6234d11f, 0xfea6c48a, 0x532e349d, 0x55f3a2a0, 0xe18a0532, 0xebf6a475, 0xec830b39, 0xef6040aa, + 0x9f715e06, 0x106ebd51, 0x8a213ef9, 0x06dd963d, 0x053eddae, 0xbde64d46, 0x8d5491b5, 0x5dc47105, 0xd406046f, + 0x155060ff, 0xfb981924, 0xe9bdd697, 0x434089cc, 0x9ed96777, 0x42e8b0bd, 0x8b890788, 0x5b19e738, 0xeec879db, + 0x0a7ca147, 0x0f427ce9, 0x1e84f8c9, 0x00000000, 0x86800983, 0xed2b3248, 0x70111eac, 0x725a6c4e, 0xff0efdfb, + 0x38850f56, 0xd5ae3d1e, 0x392d3627, 0xd90f0a64, 0xa65c6821, 0x545b9bd1, 0x2e36243a, 0x670a0cb1, 0xe757930f, + 0x96eeb4d2, 0x919b1b9e, 0xc5c0804f, 0x20dc61a2, 0x4b775a69, 0x1a121c16, 0xba93e20a, 0x2aa0c0e5, 0xe0223c43, + 0x171b121d, 0x0d090e0b, 0xc78bf2ad, 0xa8b62db9, 0xa91e14c8, 0x19f15785, 0x0775af4c, 0xdd99eebb, 0x607fa3fd, + 0x2601f79f, 0xf5725cbc, 0x3b6644c5, 0x7efb5b34, 0x29438b76, 0xc623cbdc, 0xfcedb668, 0xf1e4b863, 0xdc31d7ca, + 0x85634210, 0x22971340, 0x11c68420, 0x244a857d, 0x3dbbd2f8, 0x32f9ae11, 0xa129c76d, 0x2f9e1d4b, 0x30b2dcf3, + 0x52860dec, 0xe3c177d0, 0x16b32b6c, 0xb970a999, 0x489411fa, 0x64e94722, 0x8cfca8c4, 0x3ff0a01a, 0x2c7d56d8, + 0x903322ef, 0x4e4987c7, 0xd138d9c1, 0xa2ca8cfe, 0x0bd49836, 0x81f5a6cf, 0xde7aa528, 0x8eb7da26, 0xbfad3fa4, + 0x9d3a2ce4, 0x9278500d, 0xcc5f6a9b, 0x467e5462, 0x138df6c2, 0xb8d890e8, 0xf7392e5e, 0xafc382f5, 0x805d9fbe, + 0x93d0697c, 0x2dd56fa9, 0x1225cfb3, 0x99acc83b, 0x7d1810a7, 0x639ce86e, 0xbb3bdb7b, 0x7826cd09, 0x18596ef4, + 0xb79aec01, 0x9a4f83a8, 0x6e95e665, 0xe6ffaa7e, 0xcfbc2108, 0xe815efe6, 0x9be7bad9, 0x366f4ace, 0x099fead4, + 0x7cb029d6, 0xb2a431af, 0x233f2a31, 0x94a5c630, 0x66a235c0, 0xbc4e7437, 0xca82fca6, 0xd090e0b0, 0xd8a73315, + 0x9804f14a, 0xdaec41f7, 0x50cd7f0e, 0xf691172f, 0xd64d768d, 0xb0ef434d, 0x4daacc54, 0x0496e4df, 0xb5d19ee3, + 0x886a4c1b, 0x1f2cc1b8, 0x5165467f, 0xea5e9d04, 0x358c015d, 0x7487fa73, 0x410bfb2e, 0x1d67b35a, 0xd2db9252, + 0x5610e933, 0x47d66d13, 0x61d79a8c, 0x0ca1377a, 0x14f8598e, 0x3c13eb89, 0x27a9ceee, 0xc961b735, 0xe51ce1ed, + 0xb1477a3c, 0xdfd29c59, 0x73f2553f, 0xce141879, 0x37c773bf, 0xcdf753ea, 0xaafd5f5b, 0x6f3ddf14, 0xdb447886, + 0xf3afca81, 0xc468b93e, 0x3424382c, 0x40a3c25f, 0xc31d1672, 0x25e2bc0c, 0x493c288b, 0x950dff41, 0x01a83971, + 0xb30c08de, 0xe4b4d89c, 0xc1566490, 0x84cb7b61, 0xb632d570, 0x5c6c4874, 0x57b8d042 }; + + private static final int[] Tinv2 = { 0xf45150a7, 0x417e5365, 0x171ac3a4, 0x273a965e, 0xab3bcb6b, 0x9d1ff145, + 0xfaacab58, 0xe34b9303, 0x302055fa, 0x76adf66d, 0xcc889176, 0x02f5254c, 0xe54ffcd7, 0x2ac5d7cb, 0x35268044, + 0x62b58fa3, 0xb1de495a, 0xba25671b, 0xea45980e, 0xfe5de1c0, 0x2fc30275, 0x4c8112f0, 0x468da397, 0xd36bc6f9, + 0x8f03e75f, 0x9215959c, 0x6dbfeb7a, 0x5295da59, 0xbed42d83, 0x7458d321, 0xe0492969, 0xc98e44c8, 0xc2756a89, + 0x8ef47879, 0x58996b3e, 0xb927dd71, 0xe1beb64f, 0x88f017ad, 0x20c966ac, 0xce7db43a, 0xdf63184a, 0x1ae58231, + 0x51976033, 0x5362457f, 0x64b1e077, 0x6bbb84ae, 0x81fe1ca0, 0x08f9942b, 0x48705868, 0x458f19fd, 0xde94876c, + 0x7b52b7f8, 0x73ab23d3, 0x4b72e202, 0x1fe3578f, 0x55662aab, 0xebb20728, 0xb52f03c2, 0xc5869a7b, 0x37d3a508, + 0x2830f287, 0xbf23b2a5, 0x0302ba6a, 0x16ed5c82, 0xcf8a2b1c, 0x79a792b4, 0x07f3f0f2, 0x694ea1e2, 0xda65cdf4, + 0x0506d5be, 0x34d11f62, 0xa6c48afe, 0x2e349d53, 0xf3a2a055, 0x8a0532e1, 0xf6a475eb, 0x830b39ec, 0x6040aaef, + 0x715e069f, 0x6ebd5110, 0x213ef98a, 0xdd963d06, 0x3eddae05, 0xe64d46bd, 0x5491b58d, 0xc471055d, 0x06046fd4, + 0x5060ff15, 0x981924fb, 0xbdd697e9, 0x4089cc43, 0xd967779e, 0xe8b0bd42, 0x8907888b, 0x19e7385b, 0xc879dbee, + 0x7ca1470a, 0x427ce90f, 0x84f8c91e, 0x00000000, 0x80098386, 0x2b3248ed, 0x111eac70, 0x5a6c4e72, 0x0efdfbff, + 0x850f5638, 0xae3d1ed5, 0x2d362739, 0x0f0a64d9, 0x5c6821a6, 0x5b9bd154, 0x36243a2e, 0x0a0cb167, 0x57930fe7, + 0xeeb4d296, 0x9b1b9e91, 0xc0804fc5, 0xdc61a220, 0x775a694b, 0x121c161a, 0x93e20aba, 0xa0c0e52a, 0x223c43e0, + 0x1b121d17, 0x090e0b0d, 0x8bf2adc7, 0xb62db9a8, 0x1e14c8a9, 0xf1578519, 0x75af4c07, 0x99eebbdd, 0x7fa3fd60, + 0x01f79f26, 0x725cbcf5, 0x6644c53b, 0xfb5b347e, 0x438b7629, 0x23cbdcc6, 0xedb668fc, 0xe4b863f1, 0x31d7cadc, + 0x63421085, 0x97134022, 0xc6842011, 0x4a857d24, 0xbbd2f83d, 0xf9ae1132, 0x29c76da1, 0x9e1d4b2f, 0xb2dcf330, + 0x860dec52, 0xc177d0e3, 0xb32b6c16, 0x70a999b9, 0x9411fa48, 0xe9472264, 0xfca8c48c, 0xf0a01a3f, 0x7d56d82c, + 0x3322ef90, 0x4987c74e, 0x38d9c1d1, 0xca8cfea2, 0xd498360b, 0xf5a6cf81, 0x7aa528de, 0xb7da268e, 0xad3fa4bf, + 0x3a2ce49d, 0x78500d92, 0x5f6a9bcc, 0x7e546246, 0x8df6c213, 0xd890e8b8, 0x392e5ef7, 0xc382f5af, 0x5d9fbe80, + 0xd0697c93, 0xd56fa92d, 0x25cfb312, 0xacc83b99, 0x1810a77d, 0x9ce86e63, 0x3bdb7bbb, 0x26cd0978, 0x596ef418, + 0x9aec01b7, 0x4f83a89a, 0x95e6656e, 0xffaa7ee6, 0xbc2108cf, 0x15efe6e8, 0xe7bad99b, 0x6f4ace36, 0x9fead409, + 0xb029d67c, 0xa431afb2, 0x3f2a3123, 0xa5c63094, 0xa235c066, 0x4e7437bc, 0x82fca6ca, 0x90e0b0d0, 0xa73315d8, + 0x04f14a98, 0xec41f7da, 0xcd7f0e50, 0x91172ff6, 0x4d768dd6, 0xef434db0, 0xaacc544d, 0x96e4df04, 0xd19ee3b5, + 0x6a4c1b88, 0x2cc1b81f, 0x65467f51, 0x5e9d04ea, 0x8c015d35, 0x87fa7374, 0x0bfb2e41, 0x67b35a1d, 0xdb9252d2, + 0x10e93356, 0xd66d1347, 0xd79a8c61, 0xa1377a0c, 0xf8598e14, 0x13eb893c, 0xa9ceee27, 0x61b735c9, 0x1ce1ede5, + 0x477a3cb1, 0xd29c59df, 0xf2553f73, 0x141879ce, 0xc773bf37, 0xf753eacd, 0xfd5f5baa, 0x3ddf146f, 0x447886db, + 0xafca81f3, 0x68b93ec4, 0x24382c34, 0xa3c25f40, 0x1d1672c3, 0xe2bc0c25, 0x3c288b49, 0x0dff4195, 0xa8397101, + 0x0c08deb3, 0xb4d89ce4, 0x566490c1, 0xcb7b6184, 0x32d570b6, 0x6c48745c, 0xb8d04257 }; + + private static final int[] Tinv3 = { 0x5150a7f4, 0x7e536541, 0x1ac3a417, 0x3a965e27, 0x3bcb6bab, 0x1ff1459d, + 0xacab58fa, 0x4b9303e3, 0x2055fa30, 0xadf66d76, 0x889176cc, 0xf5254c02, 0x4ffcd7e5, 0xc5d7cb2a, 0x26804435, + 0xb58fa362, 0xde495ab1, 0x25671bba, 0x45980eea, 0x5de1c0fe, 0xc302752f, 0x8112f04c, 0x8da39746, 0x6bc6f9d3, + 0x03e75f8f, 0x15959c92, 0xbfeb7a6d, 0x95da5952, 0xd42d83be, 0x58d32174, 0x492969e0, 0x8e44c8c9, 0x756a89c2, + 0xf478798e, 0x996b3e58, 0x27dd71b9, 0xbeb64fe1, 0xf017ad88, 0xc966ac20, 0x7db43ace, 0x63184adf, 0xe582311a, + 0x97603351, 0x62457f53, 0xb1e07764, 0xbb84ae6b, 0xfe1ca081, 0xf9942b08, 0x70586848, 0x8f19fd45, 0x94876cde, + 0x52b7f87b, 0xab23d373, 0x72e2024b, 0xe3578f1f, 0x662aab55, 0xb20728eb, 0x2f03c2b5, 0x869a7bc5, 0xd3a50837, + 0x30f28728, 0x23b2a5bf, 0x02ba6a03, 0xed5c8216, 0x8a2b1ccf, 0xa792b479, 0xf3f0f207, 0x4ea1e269, 0x65cdf4da, + 0x06d5be05, 0xd11f6234, 0xc48afea6, 0x349d532e, 0xa2a055f3, 0x0532e18a, 0xa475ebf6, 0x0b39ec83, 0x40aaef60, + 0x5e069f71, 0xbd51106e, 0x3ef98a21, 0x963d06dd, 0xddae053e, 0x4d46bde6, 0x91b58d54, 0x71055dc4, 0x046fd406, + 0x60ff1550, 0x1924fb98, 0xd697e9bd, 0x89cc4340, 0x67779ed9, 0xb0bd42e8, 0x07888b89, 0xe7385b19, 0x79dbeec8, + 0xa1470a7c, 0x7ce90f42, 0xf8c91e84, 0x00000000, 0x09838680, 0x3248ed2b, 0x1eac7011, 0x6c4e725a, 0xfdfbff0e, + 0x0f563885, 0x3d1ed5ae, 0x3627392d, 0x0a64d90f, 0x6821a65c, 0x9bd1545b, 0x243a2e36, 0x0cb1670a, 0x930fe757, + 0xb4d296ee, 0x1b9e919b, 0x804fc5c0, 0x61a220dc, 0x5a694b77, 0x1c161a12, 0xe20aba93, 0xc0e52aa0, 0x3c43e022, + 0x121d171b, 0x0e0b0d09, 0xf2adc78b, 0x2db9a8b6, 0x14c8a91e, 0x578519f1, 0xaf4c0775, 0xeebbdd99, 0xa3fd607f, + 0xf79f2601, 0x5cbcf572, 0x44c53b66, 0x5b347efb, 0x8b762943, 0xcbdcc623, 0xb668fced, 0xb863f1e4, 0xd7cadc31, + 0x42108563, 0x13402297, 0x842011c6, 0x857d244a, 0xd2f83dbb, 0xae1132f9, 0xc76da129, 0x1d4b2f9e, 0xdcf330b2, + 0x0dec5286, 0x77d0e3c1, 0x2b6c16b3, 0xa999b970, 0x11fa4894, 0x472264e9, 0xa8c48cfc, 0xa01a3ff0, 0x56d82c7d, + 0x22ef9033, 0x87c74e49, 0xd9c1d138, 0x8cfea2ca, 0x98360bd4, 0xa6cf81f5, 0xa528de7a, 0xda268eb7, 0x3fa4bfad, + 0x2ce49d3a, 0x500d9278, 0x6a9bcc5f, 0x5462467e, 0xf6c2138d, 0x90e8b8d8, 0x2e5ef739, 0x82f5afc3, 0x9fbe805d, + 0x697c93d0, 0x6fa92dd5, 0xcfb31225, 0xc83b99ac, 0x10a77d18, 0xe86e639c, 0xdb7bbb3b, 0xcd097826, 0x6ef41859, + 0xec01b79a, 0x83a89a4f, 0xe6656e95, 0xaa7ee6ff, 0x2108cfbc, 0xefe6e815, 0xbad99be7, 0x4ace366f, 0xead4099f, + 0x29d67cb0, 0x31afb2a4, 0x2a31233f, 0xc63094a5, 0x35c066a2, 0x7437bc4e, 0xfca6ca82, 0xe0b0d090, 0x3315d8a7, + 0xf14a9804, 0x41f7daec, 0x7f0e50cd, 0x172ff691, 0x768dd64d, 0x434db0ef, 0xcc544daa, 0xe4df0496, 0x9ee3b5d1, + 0x4c1b886a, 0xc1b81f2c, 0x467f5165, 0x9d04ea5e, 0x015d358c, 0xfa737487, 0xfb2e410b, 0xb35a1d67, 0x9252d2db, + 0xe9335610, 0x6d1347d6, 0x9a8c61d7, 0x377a0ca1, 0x598e14f8, 0xeb893c13, 0xceee27a9, 0xb735c961, 0xe1ede51c, + 0x7a3cb147, 0x9c59dfd2, 0x553f73f2, 0x1879ce14, 0x73bf37c7, 0x53eacdf7, 0x5f5baafd, 0xdf146f3d, 0x7886db44, + 0xca81f3af, 0xb93ec468, 0x382c3424, 0xc25f40a3, 0x1672c31d, 0xbc0c25e2, 0x288b493c, 0xff41950d, 0x397101a8, + 0x08deb30c, 0xd89ce4b4, 0x6490c156, 0x7b6184cb, 0xd570b632, 0x48745c6c, 0xd04257b8 }; + + private final int shift(int r, int shift) + { + return (((r >>> shift) | (r << (32 - shift)))); + } + + /* multiply four bytes in GF(2^8) by 'x' {02} in parallel */ + + private static final int m1 = 0x80808080; + private static final int m2 = 0x7f7f7f7f; + private static final int m3 = 0x0000001b; + + private final int FFmulX(int x) + { + return (((x & m2) << 1) ^ (((x & m1) >>> 7) * m3)); + } + + /* + * The following defines provide alternative definitions of FFmulX that + * might give improved performance if a fast 32-bit multiply is not + * available. + * + * private int FFmulX(int x) { int u = x & m1; u |= (u >> 1); return ((x & + * m2) < < 1) ^ ((u >>> 3) | (u >>> 6)); } private static final int m4 = + * 0x1b1b1b1b; private int FFmulX(int x) { int u = x & m1; return ((x & m2) < < + * 1) ^ ((u - (u >>> 7)) & m4); } + * + */ + + private final int inv_mcol(int x) + { + int f2 = FFmulX(x); + int f4 = FFmulX(f2); + int f8 = FFmulX(f4); + int f9 = x ^ f8; + + return f2 ^ f4 ^ f8 ^ shift(f2 ^ f9, 8) ^ shift(f4 ^ f9, 16) ^ shift(f9, 24); + } + + private final int subWord(int x) + { + return (S[x & 255] & 255 | ((S[(x >> 8) & 255] & 255) << 8) | ((S[(x >> 16) & 255] & 255) << 16) | S[(x >> 24) & 255] << 24); + } + + /** + * Calculate the necessary round keys The number of calculations depends on + * key size and block size AES specified a fixed block size of 128 bits and + * key sizes 128/192/256 bits This code is written assuming those are the + * only possible values + */ + private final int[][] generateWorkingKey(byte[] key, boolean forEncryption) + { + int KC = key.length / 4; // key length in words + int t; + + if (((KC != 4) && (KC != 6) && (KC != 8)) || ((KC * 4) != key.length)) + { + throw new IllegalArgumentException("Key length not 128/192/256 bits."); + } + + ROUNDS = KC + 6; // This is not always true for the generalized + // Rijndael that allows larger block sizes + int[][] W = new int[ROUNDS + 1][4]; // 4 words in a block + + // + // copy the key into the round key array + // + + t = 0; + for (int i = 0; i < key.length; t++) + { + W[t >> 2][t & 3] = (key[i] & 0xff) | ((key[i + 1] & 0xff) << 8) | ((key[i + 2] & 0xff) << 16) + | (key[i + 3] << 24); + i += 4; + } + + // + // while not enough round key material calculated + // calculate new values + // + int k = (ROUNDS + 1) << 2; + for (int i = KC; (i < k); i++) + { + int temp = W[(i - 1) >> 2][(i - 1) & 3]; + if ((i % KC) == 0) + { + temp = subWord(shift(temp, 8)) ^ rcon[(i / KC) - 1]; + } + else if ((KC > 6) && ((i % KC) == 4)) + { + temp = subWord(temp); + } + + W[i >> 2][i & 3] = W[(i - KC) >> 2][(i - KC) & 3] ^ temp; + } + + if (!forEncryption) + { + for (int j = 1; j < ROUNDS; j++) + { + for (int i = 0; i < 4; i++) + { + W[j][i] = inv_mcol(W[j][i]); + } + } + } + + return W; + } + + private int ROUNDS; + private int[][] WorkingKey = null; + private int C0, C1, C2, C3; + private boolean doEncrypt; + + private static final int BLOCK_SIZE = 16; + + /** + * default constructor - 128 bit block size. + */ + public AES() + { + } + + /** + * initialise an AES cipher. + * + * @param forEncryption + * whether or not we are for encryption. + * @param key + * the key required to set up the cipher. + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + + public final void init(boolean forEncryption, byte[] key) + { + WorkingKey = generateWorkingKey(key, forEncryption); + this.doEncrypt = forEncryption; + } + + public final String getAlgorithmName() + { + return "AES"; + } + + public final int getBlockSize() + { + return BLOCK_SIZE; + } + + public final int processBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (WorkingKey == null) + { + throw new IllegalStateException("AES engine not initialised"); + } + + if ((inOff + (32 / 2)) > in.length) + { + throw new IllegalArgumentException("input buffer too short"); + } + + if ((outOff + (32 / 2)) > out.length) + { + throw new IllegalArgumentException("output buffer too short"); + } + + if (doEncrypt) + { + unpackBlock(in, inOff); + encryptBlock(WorkingKey); + packBlock(out, outOff); + } + else + { + unpackBlock(in, inOff); + decryptBlock(WorkingKey); + packBlock(out, outOff); + } + + return BLOCK_SIZE; + } + + public final void reset() + { + } + + private final void unpackBlock(byte[] bytes, int off) + { + int index = off; + + C0 = (bytes[index++] & 0xff); + C0 |= (bytes[index++] & 0xff) << 8; + C0 |= (bytes[index++] & 0xff) << 16; + C0 |= bytes[index++] << 24; + + C1 = (bytes[index++] & 0xff); + C1 |= (bytes[index++] & 0xff) << 8; + C1 |= (bytes[index++] & 0xff) << 16; + C1 |= bytes[index++] << 24; + + C2 = (bytes[index++] & 0xff); + C2 |= (bytes[index++] & 0xff) << 8; + C2 |= (bytes[index++] & 0xff) << 16; + C2 |= bytes[index++] << 24; + + C3 = (bytes[index++] & 0xff); + C3 |= (bytes[index++] & 0xff) << 8; + C3 |= (bytes[index++] & 0xff) << 16; + C3 |= bytes[index++] << 24; + } + + private final void packBlock(byte[] bytes, int off) + { + int index = off; + + bytes[index++] = (byte) C0; + bytes[index++] = (byte) (C0 >> 8); + bytes[index++] = (byte) (C0 >> 16); + bytes[index++] = (byte) (C0 >> 24); + + bytes[index++] = (byte) C1; + bytes[index++] = (byte) (C1 >> 8); + bytes[index++] = (byte) (C1 >> 16); + bytes[index++] = (byte) (C1 >> 24); + + bytes[index++] = (byte) C2; + bytes[index++] = (byte) (C2 >> 8); + bytes[index++] = (byte) (C2 >> 16); + bytes[index++] = (byte) (C2 >> 24); + + bytes[index++] = (byte) C3; + bytes[index++] = (byte) (C3 >> 8); + bytes[index++] = (byte) (C3 >> 16); + bytes[index++] = (byte) (C3 >> 24); + } + + private final void encryptBlock(int[][] KW) + { + int r, r0, r1, r2, r3; + + C0 ^= KW[0][0]; + C1 ^= KW[0][1]; + C2 ^= KW[0][2]; + C3 ^= KW[0][3]; + + for (r = 1; r < ROUNDS - 1;) + { + r0 = T0[C0 & 255] ^ T1[(C1 >> 8) & 255] ^ T2[(C2 >> 16) & 255] ^ T3[(C3 >> 24) & 255] ^ KW[r][0]; + r1 = T0[C1 & 255] ^ T1[(C2 >> 8) & 255] ^ T2[(C3 >> 16) & 255] ^ T3[(C0 >> 24) & 255] ^ KW[r][1]; + r2 = T0[C2 & 255] ^ T1[(C3 >> 8) & 255] ^ T2[(C0 >> 16) & 255] ^ T3[(C1 >> 24) & 255] ^ KW[r][2]; + r3 = T0[C3 & 255] ^ T1[(C0 >> 8) & 255] ^ T2[(C1 >> 16) & 255] ^ T3[(C2 >> 24) & 255] ^ KW[r++][3]; + C0 = T0[r0 & 255] ^ T1[(r1 >> 8) & 255] ^ T2[(r2 >> 16) & 255] ^ T3[(r3 >> 24) & 255] ^ KW[r][0]; + C1 = T0[r1 & 255] ^ T1[(r2 >> 8) & 255] ^ T2[(r3 >> 16) & 255] ^ T3[(r0 >> 24) & 255] ^ KW[r][1]; + C2 = T0[r2 & 255] ^ T1[(r3 >> 8) & 255] ^ T2[(r0 >> 16) & 255] ^ T3[(r1 >> 24) & 255] ^ KW[r][2]; + C3 = T0[r3 & 255] ^ T1[(r0 >> 8) & 255] ^ T2[(r1 >> 16) & 255] ^ T3[(r2 >> 24) & 255] ^ KW[r++][3]; + } + + r0 = T0[C0 & 255] ^ T1[(C1 >> 8) & 255] ^ T2[(C2 >> 16) & 255] ^ T3[(C3 >> 24) & 255] ^ KW[r][0]; + r1 = T0[C1 & 255] ^ T1[(C2 >> 8) & 255] ^ T2[(C3 >> 16) & 255] ^ T3[(C0 >> 24) & 255] ^ KW[r][1]; + r2 = T0[C2 & 255] ^ T1[(C3 >> 8) & 255] ^ T2[(C0 >> 16) & 255] ^ T3[(C1 >> 24) & 255] ^ KW[r][2]; + r3 = T0[C3 & 255] ^ T1[(C0 >> 8) & 255] ^ T2[(C1 >> 16) & 255] ^ T3[(C2 >> 24) & 255] ^ KW[r++][3]; + + // the final round's table is a simple function of S so we don't use a + // whole other four tables for it + + C0 = (S[r0 & 255] & 255) ^ ((S[(r1 >> 8) & 255] & 255) << 8) ^ ((S[(r2 >> 16) & 255] & 255) << 16) + ^ (S[(r3 >> 24) & 255] << 24) ^ KW[r][0]; + C1 = (S[r1 & 255] & 255) ^ ((S[(r2 >> 8) & 255] & 255) << 8) ^ ((S[(r3 >> 16) & 255] & 255) << 16) + ^ (S[(r0 >> 24) & 255] << 24) ^ KW[r][1]; + C2 = (S[r2 & 255] & 255) ^ ((S[(r3 >> 8) & 255] & 255) << 8) ^ ((S[(r0 >> 16) & 255] & 255) << 16) + ^ (S[(r1 >> 24) & 255] << 24) ^ KW[r][2]; + C3 = (S[r3 & 255] & 255) ^ ((S[(r0 >> 8) & 255] & 255) << 8) ^ ((S[(r1 >> 16) & 255] & 255) << 16) + ^ (S[(r2 >> 24) & 255] << 24) ^ KW[r][3]; + + } + + private final void decryptBlock(int[][] KW) + { + int r, r0, r1, r2, r3; + + C0 ^= KW[ROUNDS][0]; + C1 ^= KW[ROUNDS][1]; + C2 ^= KW[ROUNDS][2]; + C3 ^= KW[ROUNDS][3]; + + for (r = ROUNDS - 1; r > 1;) + { + r0 = Tinv0[C0 & 255] ^ Tinv1[(C3 >> 8) & 255] ^ Tinv2[(C2 >> 16) & 255] ^ Tinv3[(C1 >> 24) & 255] + ^ KW[r][0]; + r1 = Tinv0[C1 & 255] ^ Tinv1[(C0 >> 8) & 255] ^ Tinv2[(C3 >> 16) & 255] ^ Tinv3[(C2 >> 24) & 255] + ^ KW[r][1]; + r2 = Tinv0[C2 & 255] ^ Tinv1[(C1 >> 8) & 255] ^ Tinv2[(C0 >> 16) & 255] ^ Tinv3[(C3 >> 24) & 255] + ^ KW[r][2]; + r3 = Tinv0[C3 & 255] ^ Tinv1[(C2 >> 8) & 255] ^ Tinv2[(C1 >> 16) & 255] ^ Tinv3[(C0 >> 24) & 255] + ^ KW[r--][3]; + C0 = Tinv0[r0 & 255] ^ Tinv1[(r3 >> 8) & 255] ^ Tinv2[(r2 >> 16) & 255] ^ Tinv3[(r1 >> 24) & 255] + ^ KW[r][0]; + C1 = Tinv0[r1 & 255] ^ Tinv1[(r0 >> 8) & 255] ^ Tinv2[(r3 >> 16) & 255] ^ Tinv3[(r2 >> 24) & 255] + ^ KW[r][1]; + C2 = Tinv0[r2 & 255] ^ Tinv1[(r1 >> 8) & 255] ^ Tinv2[(r0 >> 16) & 255] ^ Tinv3[(r3 >> 24) & 255] + ^ KW[r][2]; + C3 = Tinv0[r3 & 255] ^ Tinv1[(r2 >> 8) & 255] ^ Tinv2[(r1 >> 16) & 255] ^ Tinv3[(r0 >> 24) & 255] + ^ KW[r--][3]; + } + + r0 = Tinv0[C0 & 255] ^ Tinv1[(C3 >> 8) & 255] ^ Tinv2[(C2 >> 16) & 255] ^ Tinv3[(C1 >> 24) & 255] ^ KW[r][0]; + r1 = Tinv0[C1 & 255] ^ Tinv1[(C0 >> 8) & 255] ^ Tinv2[(C3 >> 16) & 255] ^ Tinv3[(C2 >> 24) & 255] ^ KW[r][1]; + r2 = Tinv0[C2 & 255] ^ Tinv1[(C1 >> 8) & 255] ^ Tinv2[(C0 >> 16) & 255] ^ Tinv3[(C3 >> 24) & 255] ^ KW[r][2]; + r3 = Tinv0[C3 & 255] ^ Tinv1[(C2 >> 8) & 255] ^ Tinv2[(C1 >> 16) & 255] ^ Tinv3[(C0 >> 24) & 255] ^ KW[r--][3]; + + // the final round's table is a simple function of Si so we don't use a + // whole other four tables for it + + C0 = (Si[r0 & 255] & 255) ^ ((Si[(r3 >> 8) & 255] & 255) << 8) ^ ((Si[(r2 >> 16) & 255] & 255) << 16) + ^ (Si[(r1 >> 24) & 255] << 24) ^ KW[0][0]; + C1 = (Si[r1 & 255] & 255) ^ ((Si[(r0 >> 8) & 255] & 255) << 8) ^ ((Si[(r3 >> 16) & 255] & 255) << 16) + ^ (Si[(r2 >> 24) & 255] << 24) ^ KW[0][1]; + C2 = (Si[r2 & 255] & 255) ^ ((Si[(r1 >> 8) & 255] & 255) << 8) ^ ((Si[(r0 >> 16) & 255] & 255) << 16) + ^ (Si[(r3 >> 24) & 255] << 24) ^ KW[0][2]; + C3 = (Si[r3 & 255] & 255) ^ ((Si[(r2 >> 8) & 255] & 255) << 8) ^ ((Si[(r1 >> 16) & 255] & 255) << 16) + ^ (Si[(r0 >> 24) & 255] << 24) ^ KW[0][3]; + } + + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + processBlock(src, srcoff, dst, dstoff); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java new file mode 100644 index 0000000..4cc28ab --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java @@ -0,0 +1,16 @@ +package com.trilead.ssh2.crypto.cipher; + +/** + * BlockCipher. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: BlockCipher.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public interface BlockCipher +{ + public void init(boolean forEncryption, byte[] key); + + public int getBlockSize(); + + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff); +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java new file mode 100644 index 0000000..6e386a5 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java @@ -0,0 +1,115 @@ + +package com.trilead.ssh2.crypto.cipher; + +import java.util.Vector; + +/** + * BlockCipherFactory. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: BlockCipherFactory.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class BlockCipherFactory +{ + static class CipherEntry + { + String type; + int blocksize; + int keysize; + String cipherClass; + + public CipherEntry(String type, int blockSize, int keySize, String cipherClass) + { + this.type = type; + this.blocksize = blockSize; + this.keysize = keySize; + this.cipherClass = cipherClass; + } + } + + static Vector ciphers = new Vector(); + + static + { + /* Higher Priority First */ + + ciphers.addElement(new CipherEntry("aes256-ctr", 16, 32, "com.trilead.ssh2.crypto.cipher.AES")); + ciphers.addElement(new CipherEntry("aes192-ctr", 16, 24, "com.trilead.ssh2.crypto.cipher.AES")); + ciphers.addElement(new CipherEntry("aes128-ctr", 16, 16, "com.trilead.ssh2.crypto.cipher.AES")); + ciphers.addElement(new CipherEntry("blowfish-ctr", 8, 16, "com.trilead.ssh2.crypto.cipher.BlowFish")); + + ciphers.addElement(new CipherEntry("aes256-cbc", 16, 32, "com.trilead.ssh2.crypto.cipher.AES")); + ciphers.addElement(new CipherEntry("aes192-cbc", 16, 24, "com.trilead.ssh2.crypto.cipher.AES")); + ciphers.addElement(new CipherEntry("aes128-cbc", 16, 16, "com.trilead.ssh2.crypto.cipher.AES")); + ciphers.addElement(new CipherEntry("blowfish-cbc", 8, 16, "com.trilead.ssh2.crypto.cipher.BlowFish")); + + ciphers.addElement(new CipherEntry("3des-ctr", 8, 24, "com.trilead.ssh2.crypto.cipher.DESede")); + ciphers.addElement(new CipherEntry("3des-cbc", 8, 24, "com.trilead.ssh2.crypto.cipher.DESede")); + } + + public static String[] getDefaultCipherList() + { + String list[] = new String[ciphers.size()]; + for (int i = 0; i < ciphers.size(); i++) + { + CipherEntry ce = ciphers.elementAt(i); + list[i] = new String(ce.type); + } + return list; + } + + public static void checkCipherList(String[] cipherCandidates) + { + for (int i = 0; i < cipherCandidates.length; i++) + getEntry(cipherCandidates[i]); + } + + public static BlockCipher createCipher(String type, boolean encrypt, byte[] key, byte[] iv) + { + try + { + CipherEntry ce = getEntry(type); + Class cc = Class.forName(ce.cipherClass); + BlockCipher bc = (BlockCipher) cc.newInstance(); + + if (type.endsWith("-cbc")) + { + bc.init(encrypt, key); + return new CBCMode(bc, iv, encrypt); + } + else if (type.endsWith("-ctr")) + { + bc.init(true, key); + return new CTRMode(bc, iv, encrypt); + } + throw new IllegalArgumentException("Cannot instantiate " + type); + } + catch (Exception e) + { + throw new IllegalArgumentException("Cannot instantiate " + type); + } + } + + private static CipherEntry getEntry(String type) + { + for (int i = 0; i < ciphers.size(); i++) + { + CipherEntry ce = ciphers.elementAt(i); + if (ce.type.equals(type)) + return ce; + } + throw new IllegalArgumentException("Unkown algorithm " + type); + } + + public static int getBlockSize(String type) + { + CipherEntry ce = getEntry(type); + return ce.blocksize; + } + + public static int getKeySize(String type) + { + CipherEntry ce = getEntry(type); + return ce.keysize; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java new file mode 100644 index 0000000..0b2f295 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java @@ -0,0 +1,403 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + This file was shamelessly taken from the Bouncy Castle Crypto package. + Their licence file states the following: + + Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + (http://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + * A class that provides Blowfish key encryption operations, such as encoding + * data and generating keys. All the algorithms herein are from Applied + * Cryptography and implement a simplified cryptography interface. + * + * @author See comments in the source file + * @version $Id: BlowFish.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class BlowFish implements BlockCipher +{ + + private final static int[] KP = { 0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344, 0xA4093822, 0x299F31D0, + 0x082EFA98, 0xEC4E6C89, 0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C, 0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, + 0xB5470917, 0x9216D5D9, 0x8979FB1B }, + + KS0 = { 0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7, 0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99, 0x24A19947, + 0xB3916CF7, 0x0801F2E2, 0x858EFC16, 0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E, 0x0D95748F, 0x728EB658, + 0x718BCD58, 0x82154AEE, 0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013, 0xC5D1B023, 0x286085F0, 0xCA417918, + 0xB8DB38EF, 0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E, 0xD71577C1, 0xBD314B27, 0x78AF2FDA, 0x55605C60, + 0xE65525F3, 0xAA55AB94, 0x57489862, 0x63E81440, 0x55CA396A, 0x2AAB10B6, 0xB4CC5C34, 0x1141E8CE, 0xA15486AF, + 0x7C72E993, 0xB3EE1411, 0x636FBC2A, 0x2BA9C55D, 0x741831F6, 0xCE5C3E16, 0x9B87931E, 0xAFD6BA33, 0x6C24CF5C, + 0x7A325381, 0x28958677, 0x3B8F4898, 0x6B4BB9AF, 0xC4BFE81B, 0x66282193, 0x61D809CC, 0xFB21A991, 0x487CAC60, + 0x5DEC8032, 0xEF845D5D, 0xE98575B1, 0xDC262302, 0xEB651B88, 0x23893E81, 0xD396ACC5, 0x0F6D6FF3, 0x83F44239, + 0x2E0B4482, 0xA4842004, 0x69C8F04A, 0x9E1F9B5E, 0x21C66842, 0xF6E96C9A, 0x670C9C61, 0xABD388F0, 0x6A51A0D2, + 0xD8542F68, 0x960FA728, 0xAB5133A3, 0x6EEF0B6C, 0x137A3BE4, 0xBA3BF050, 0x7EFB2A98, 0xA1F1651D, 0x39AF0176, + 0x66CA593E, 0x82430E88, 0x8CEE8619, 0x456F9FB4, 0x7D84A5C3, 0x3B8B5EBE, 0xE06F75D8, 0x85C12073, 0x401A449F, + 0x56C16AA6, 0x4ED3AA62, 0x363F7706, 0x1BFEDF72, 0x429B023D, 0x37D0D724, 0xD00A1248, 0xDB0FEAD3, 0x49F1C09B, + 0x075372C9, 0x80991B7B, 0x25D479D8, 0xF6E8DEF7, 0xE3FE501A, 0xB6794C3B, 0x976CE0BD, 0x04C006BA, 0xC1A94FB6, + 0x409F60C4, 0x5E5C9EC2, 0x196A2463, 0x68FB6FAF, 0x3E6C53B5, 0x1339B2EB, 0x3B52EC6F, 0x6DFC511F, 0x9B30952C, + 0xCC814544, 0xAF5EBD09, 0xBEE3D004, 0xDE334AFD, 0x660F2807, 0x192E4BB3, 0xC0CBA857, 0x45C8740F, 0xD20B5F39, + 0xB9D3FBDB, 0x5579C0BD, 0x1A60320A, 0xD6A100C6, 0x402C7279, 0x679F25FE, 0xFB1FA3CC, 0x8EA5E9F8, 0xDB3222F8, + 0x3C7516DF, 0xFD616B15, 0x2F501EC8, 0xAD0552AB, 0x323DB5FA, 0xFD238760, 0x53317B48, 0x3E00DF82, 0x9E5C57BB, + 0xCA6F8CA0, 0x1A87562E, 0xDF1769DB, 0xD542A8F6, 0x287EFFC3, 0xAC6732C6, 0x8C4F5573, 0x695B27B0, 0xBBCA58C8, + 0xE1FFA35D, 0xB8F011A0, 0x10FA3D98, 0xFD2183B8, 0x4AFCB56C, 0x2DD1D35B, 0x9A53E479, 0xB6F84565, 0xD28E49BC, + 0x4BFB9790, 0xE1DDF2DA, 0xA4CB7E33, 0x62FB1341, 0xCEE4C6E8, 0xEF20CADA, 0x36774C01, 0xD07E9EFE, 0x2BF11FB4, + 0x95DBDA4D, 0xAE909198, 0xEAAD8E71, 0x6B93D5A0, 0xD08ED1D0, 0xAFC725E0, 0x8E3C5B2F, 0x8E7594B7, 0x8FF6E2FB, + 0xF2122B64, 0x8888B812, 0x900DF01C, 0x4FAD5EA0, 0x688FC31C, 0xD1CFF191, 0xB3A8C1AD, 0x2F2F2218, 0xBE0E1777, + 0xEA752DFE, 0x8B021FA1, 0xE5A0CC0F, 0xB56F74E8, 0x18ACF3D6, 0xCE89E299, 0xB4A84FE0, 0xFD13E0B7, 0x7CC43B81, + 0xD2ADA8D9, 0x165FA266, 0x80957705, 0x93CC7314, 0x211A1477, 0xE6AD2065, 0x77B5FA86, 0xC75442F5, 0xFB9D35CF, + 0xEBCDAF0C, 0x7B3E89A0, 0xD6411BD3, 0xAE1E7E49, 0x00250E2D, 0x2071B35E, 0x226800BB, 0x57B8E0AF, 0x2464369B, + 0xF009B91E, 0x5563911D, 0x59DFA6AA, 0x78C14389, 0xD95A537F, 0x207D5BA2, 0x02E5B9C5, 0x83260376, 0x6295CFA9, + 0x11C81968, 0x4E734A41, 0xB3472DCA, 0x7B14A94A, 0x1B510052, 0x9A532915, 0xD60F573F, 0xBC9BC6E4, 0x2B60A476, + 0x81E67400, 0x08BA6FB5, 0x571BE91F, 0xF296EC6B, 0x2A0DD915, 0xB6636521, 0xE7B9F9B6, 0xFF34052E, 0xC5855664, + 0x53B02D5D, 0xA99F8FA1, 0x08BA4799, 0x6E85076A }, + + KS1 = { 0x4B7A70E9, 0xB5B32944, 0xDB75092E, 0xC4192623, 0xAD6EA6B0, 0x49A7DF7D, 0x9CEE60B8, 0x8FEDB266, 0xECAA8C71, + 0x699A17FF, 0x5664526C, 0xC2B19EE1, 0x193602A5, 0x75094C29, 0xA0591340, 0xE4183A3E, 0x3F54989A, 0x5B429D65, + 0x6B8FE4D6, 0x99F73FD6, 0xA1D29C07, 0xEFE830F5, 0x4D2D38E6, 0xF0255DC1, 0x4CDD2086, 0x8470EB26, 0x6382E9C6, + 0x021ECC5E, 0x09686B3F, 0x3EBAEFC9, 0x3C971814, 0x6B6A70A1, 0x687F3584, 0x52A0E286, 0xB79C5305, 0xAA500737, + 0x3E07841C, 0x7FDEAE5C, 0x8E7D44EC, 0x5716F2B8, 0xB03ADA37, 0xF0500C0D, 0xF01C1F04, 0x0200B3FF, 0xAE0CF51A, + 0x3CB574B2, 0x25837A58, 0xDC0921BD, 0xD19113F9, 0x7CA92FF6, 0x94324773, 0x22F54701, 0x3AE5E581, 0x37C2DADC, + 0xC8B57634, 0x9AF3DDA7, 0xA9446146, 0x0FD0030E, 0xECC8C73E, 0xA4751E41, 0xE238CD99, 0x3BEA0E2F, 0x3280BBA1, + 0x183EB331, 0x4E548B38, 0x4F6DB908, 0x6F420D03, 0xF60A04BF, 0x2CB81290, 0x24977C79, 0x5679B072, 0xBCAF89AF, + 0xDE9A771F, 0xD9930810, 0xB38BAE12, 0xDCCF3F2E, 0x5512721F, 0x2E6B7124, 0x501ADDE6, 0x9F84CD87, 0x7A584718, + 0x7408DA17, 0xBC9F9ABC, 0xE94B7D8C, 0xEC7AEC3A, 0xDB851DFA, 0x63094366, 0xC464C3D2, 0xEF1C1847, 0x3215D908, + 0xDD433B37, 0x24C2BA16, 0x12A14D43, 0x2A65C451, 0x50940002, 0x133AE4DD, 0x71DFF89E, 0x10314E55, 0x81AC77D6, + 0x5F11199B, 0x043556F1, 0xD7A3C76B, 0x3C11183B, 0x5924A509, 0xF28FE6ED, 0x97F1FBFA, 0x9EBABF2C, 0x1E153C6E, + 0x86E34570, 0xEAE96FB1, 0x860E5E0A, 0x5A3E2AB3, 0x771FE71C, 0x4E3D06FA, 0x2965DCB9, 0x99E71D0F, 0x803E89D6, + 0x5266C825, 0x2E4CC978, 0x9C10B36A, 0xC6150EBA, 0x94E2EA78, 0xA5FC3C53, 0x1E0A2DF4, 0xF2F74EA7, 0x361D2B3D, + 0x1939260F, 0x19C27960, 0x5223A708, 0xF71312B6, 0xEBADFE6E, 0xEAC31F66, 0xE3BC4595, 0xA67BC883, 0xB17F37D1, + 0x018CFF28, 0xC332DDEF, 0xBE6C5AA5, 0x65582185, 0x68AB9802, 0xEECEA50F, 0xDB2F953B, 0x2AEF7DAD, 0x5B6E2F84, + 0x1521B628, 0x29076170, 0xECDD4775, 0x619F1510, 0x13CCA830, 0xEB61BD96, 0x0334FE1E, 0xAA0363CF, 0xB5735C90, + 0x4C70A239, 0xD59E9E0B, 0xCBAADE14, 0xEECC86BC, 0x60622CA7, 0x9CAB5CAB, 0xB2F3846E, 0x648B1EAF, 0x19BDF0CA, + 0xA02369B9, 0x655ABB50, 0x40685A32, 0x3C2AB4B3, 0x319EE9D5, 0xC021B8F7, 0x9B540B19, 0x875FA099, 0x95F7997E, + 0x623D7DA8, 0xF837889A, 0x97E32D77, 0x11ED935F, 0x16681281, 0x0E358829, 0xC7E61FD6, 0x96DEDFA1, 0x7858BA99, + 0x57F584A5, 0x1B227263, 0x9B83C3FF, 0x1AC24696, 0xCDB30AEB, 0x532E3054, 0x8FD948E4, 0x6DBC3128, 0x58EBF2EF, + 0x34C6FFEA, 0xFE28ED61, 0xEE7C3C73, 0x5D4A14D9, 0xE864B7E3, 0x42105D14, 0x203E13E0, 0x45EEE2B6, 0xA3AAABEA, + 0xDB6C4F15, 0xFACB4FD0, 0xC742F442, 0xEF6ABBB5, 0x654F3B1D, 0x41CD2105, 0xD81E799E, 0x86854DC7, 0xE44B476A, + 0x3D816250, 0xCF62A1F2, 0x5B8D2646, 0xFC8883A0, 0xC1C7B6A3, 0x7F1524C3, 0x69CB7492, 0x47848A0B, 0x5692B285, + 0x095BBF00, 0xAD19489D, 0x1462B174, 0x23820E00, 0x58428D2A, 0x0C55F5EA, 0x1DADF43E, 0x233F7061, 0x3372F092, + 0x8D937E41, 0xD65FECF1, 0x6C223BDB, 0x7CDE3759, 0xCBEE7460, 0x4085F2A7, 0xCE77326E, 0xA6078084, 0x19F8509E, + 0xE8EFD855, 0x61D99735, 0xA969A7AA, 0xC50C06C2, 0x5A04ABFC, 0x800BCADC, 0x9E447A2E, 0xC3453484, 0xFDD56705, + 0x0E1E9EC9, 0xDB73DBD3, 0x105588CD, 0x675FDA79, 0xE3674340, 0xC5C43465, 0x713E38D8, 0x3D28F89E, 0xF16DFF20, + 0x153E21E7, 0x8FB03D4A, 0xE6E39F2B, 0xDB83ADF7 }, + + KS2 = { 0xE93D5A68, 0x948140F7, 0xF64C261C, 0x94692934, 0x411520F7, 0x7602D4F7, 0xBCF46B2E, 0xD4A20068, 0xD4082471, + 0x3320F46A, 0x43B7D4B7, 0x500061AF, 0x1E39F62E, 0x97244546, 0x14214F74, 0xBF8B8840, 0x4D95FC1D, 0x96B591AF, + 0x70F4DDD3, 0x66A02F45, 0xBFBC09EC, 0x03BD9785, 0x7FAC6DD0, 0x31CB8504, 0x96EB27B3, 0x55FD3941, 0xDA2547E6, + 0xABCA0A9A, 0x28507825, 0x530429F4, 0x0A2C86DA, 0xE9B66DFB, 0x68DC1462, 0xD7486900, 0x680EC0A4, 0x27A18DEE, + 0x4F3FFEA2, 0xE887AD8C, 0xB58CE006, 0x7AF4D6B6, 0xAACE1E7C, 0xD3375FEC, 0xCE78A399, 0x406B2A42, 0x20FE9E35, + 0xD9F385B9, 0xEE39D7AB, 0x3B124E8B, 0x1DC9FAF7, 0x4B6D1856, 0x26A36631, 0xEAE397B2, 0x3A6EFA74, 0xDD5B4332, + 0x6841E7F7, 0xCA7820FB, 0xFB0AF54E, 0xD8FEB397, 0x454056AC, 0xBA489527, 0x55533A3A, 0x20838D87, 0xFE6BA9B7, + 0xD096954B, 0x55A867BC, 0xA1159A58, 0xCCA92963, 0x99E1DB33, 0xA62A4A56, 0x3F3125F9, 0x5EF47E1C, 0x9029317C, + 0xFDF8E802, 0x04272F70, 0x80BB155C, 0x05282CE3, 0x95C11548, 0xE4C66D22, 0x48C1133F, 0xC70F86DC, 0x07F9C9EE, + 0x41041F0F, 0x404779A4, 0x5D886E17, 0x325F51EB, 0xD59BC0D1, 0xF2BCC18F, 0x41113564, 0x257B7834, 0x602A9C60, + 0xDFF8E8A3, 0x1F636C1B, 0x0E12B4C2, 0x02E1329E, 0xAF664FD1, 0xCAD18115, 0x6B2395E0, 0x333E92E1, 0x3B240B62, + 0xEEBEB922, 0x85B2A20E, 0xE6BA0D99, 0xDE720C8C, 0x2DA2F728, 0xD0127845, 0x95B794FD, 0x647D0862, 0xE7CCF5F0, + 0x5449A36F, 0x877D48FA, 0xC39DFD27, 0xF33E8D1E, 0x0A476341, 0x992EFF74, 0x3A6F6EAB, 0xF4F8FD37, 0xA812DC60, + 0xA1EBDDF8, 0x991BE14C, 0xDB6E6B0D, 0xC67B5510, 0x6D672C37, 0x2765D43B, 0xDCD0E804, 0xF1290DC7, 0xCC00FFA3, + 0xB5390F92, 0x690FED0B, 0x667B9FFB, 0xCEDB7D9C, 0xA091CF0B, 0xD9155EA3, 0xBB132F88, 0x515BAD24, 0x7B9479BF, + 0x763BD6EB, 0x37392EB3, 0xCC115979, 0x8026E297, 0xF42E312D, 0x6842ADA7, 0xC66A2B3B, 0x12754CCC, 0x782EF11C, + 0x6A124237, 0xB79251E7, 0x06A1BBE6, 0x4BFB6350, 0x1A6B1018, 0x11CAEDFA, 0x3D25BDD8, 0xE2E1C3C9, 0x44421659, + 0x0A121386, 0xD90CEC6E, 0xD5ABEA2A, 0x64AF674E, 0xDA86A85F, 0xBEBFE988, 0x64E4C3FE, 0x9DBC8057, 0xF0F7C086, + 0x60787BF8, 0x6003604D, 0xD1FD8346, 0xF6381FB0, 0x7745AE04, 0xD736FCCC, 0x83426B33, 0xF01EAB71, 0xB0804187, + 0x3C005E5F, 0x77A057BE, 0xBDE8AE24, 0x55464299, 0xBF582E61, 0x4E58F48F, 0xF2DDFDA2, 0xF474EF38, 0x8789BDC2, + 0x5366F9C3, 0xC8B38E74, 0xB475F255, 0x46FCD9B9, 0x7AEB2661, 0x8B1DDF84, 0x846A0E79, 0x915F95E2, 0x466E598E, + 0x20B45770, 0x8CD55591, 0xC902DE4C, 0xB90BACE1, 0xBB8205D0, 0x11A86248, 0x7574A99E, 0xB77F19B6, 0xE0A9DC09, + 0x662D09A1, 0xC4324633, 0xE85A1F02, 0x09F0BE8C, 0x4A99A025, 0x1D6EFE10, 0x1AB93D1D, 0x0BA5A4DF, 0xA186F20F, + 0x2868F169, 0xDCB7DA83, 0x573906FE, 0xA1E2CE9B, 0x4FCD7F52, 0x50115E01, 0xA70683FA, 0xA002B5C4, 0x0DE6D027, + 0x9AF88C27, 0x773F8641, 0xC3604C06, 0x61A806B5, 0xF0177A28, 0xC0F586E0, 0x006058AA, 0x30DC7D62, 0x11E69ED7, + 0x2338EA63, 0x53C2DD94, 0xC2C21634, 0xBBCBEE56, 0x90BCB6DE, 0xEBFC7DA1, 0xCE591D76, 0x6F05E409, 0x4B7C0188, + 0x39720A3D, 0x7C927C24, 0x86E3725F, 0x724D9DB9, 0x1AC15BB4, 0xD39EB8FC, 0xED545578, 0x08FCA5B5, 0xD83D7CD3, + 0x4DAD0FC4, 0x1E50EF5E, 0xB161E6F8, 0xA28514D9, 0x6C51133C, 0x6FD5C7E7, 0x56E14EC4, 0x362ABFCE, 0xDDC6C837, + 0xD79A3234, 0x92638212, 0x670EFA8E, 0x406000E0 }, + + KS3 = { 0x3A39CE37, 0xD3FAF5CF, 0xABC27737, 0x5AC52D1B, 0x5CB0679E, 0x4FA33742, 0xD3822740, 0x99BC9BBE, 0xD5118E9D, + 0xBF0F7315, 0xD62D1C7E, 0xC700C47B, 0xB78C1B6B, 0x21A19045, 0xB26EB1BE, 0x6A366EB4, 0x5748AB2F, 0xBC946E79, + 0xC6A376D2, 0x6549C2C8, 0x530FF8EE, 0x468DDE7D, 0xD5730A1D, 0x4CD04DC6, 0x2939BBDB, 0xA9BA4650, 0xAC9526E8, + 0xBE5EE304, 0xA1FAD5F0, 0x6A2D519A, 0x63EF8CE2, 0x9A86EE22, 0xC089C2B8, 0x43242EF6, 0xA51E03AA, 0x9CF2D0A4, + 0x83C061BA, 0x9BE96A4D, 0x8FE51550, 0xBA645BD6, 0x2826A2F9, 0xA73A3AE1, 0x4BA99586, 0xEF5562E9, 0xC72FEFD3, + 0xF752F7DA, 0x3F046F69, 0x77FA0A59, 0x80E4A915, 0x87B08601, 0x9B09E6AD, 0x3B3EE593, 0xE990FD5A, 0x9E34D797, + 0x2CF0B7D9, 0x022B8B51, 0x96D5AC3A, 0x017DA67D, 0xD1CF3ED6, 0x7C7D2D28, 0x1F9F25CF, 0xADF2B89B, 0x5AD6B472, + 0x5A88F54C, 0xE029AC71, 0xE019A5E6, 0x47B0ACFD, 0xED93FA9B, 0xE8D3C48D, 0x283B57CC, 0xF8D56629, 0x79132E28, + 0x785F0191, 0xED756055, 0xF7960E44, 0xE3D35E8C, 0x15056DD4, 0x88F46DBA, 0x03A16125, 0x0564F0BD, 0xC3EB9E15, + 0x3C9057A2, 0x97271AEC, 0xA93A072A, 0x1B3F6D9B, 0x1E6321F5, 0xF59C66FB, 0x26DCF319, 0x7533D928, 0xB155FDF5, + 0x03563482, 0x8ABA3CBB, 0x28517711, 0xC20AD9F8, 0xABCC5167, 0xCCAD925F, 0x4DE81751, 0x3830DC8E, 0x379D5862, + 0x9320F991, 0xEA7A90C2, 0xFB3E7BCE, 0x5121CE64, 0x774FBE32, 0xA8B6E37E, 0xC3293D46, 0x48DE5369, 0x6413E680, + 0xA2AE0810, 0xDD6DB224, 0x69852DFD, 0x09072166, 0xB39A460A, 0x6445C0DD, 0x586CDECF, 0x1C20C8AE, 0x5BBEF7DD, + 0x1B588D40, 0xCCD2017F, 0x6BB4E3BB, 0xDDA26A7E, 0x3A59FF45, 0x3E350A44, 0xBCB4CDD5, 0x72EACEA8, 0xFA6484BB, + 0x8D6612AE, 0xBF3C6F47, 0xD29BE463, 0x542F5D9E, 0xAEC2771B, 0xF64E6370, 0x740E0D8D, 0xE75B1357, 0xF8721671, + 0xAF537D5D, 0x4040CB08, 0x4EB4E2CC, 0x34D2466A, 0x0115AF84, 0xE1B00428, 0x95983A1D, 0x06B89FB4, 0xCE6EA048, + 0x6F3F3B82, 0x3520AB82, 0x011A1D4B, 0x277227F8, 0x611560B1, 0xE7933FDC, 0xBB3A792B, 0x344525BD, 0xA08839E1, + 0x51CE794B, 0x2F32C9B7, 0xA01FBAC9, 0xE01CC87E, 0xBCC7D1F6, 0xCF0111C3, 0xA1E8AAC7, 0x1A908749, 0xD44FBD9A, + 0xD0DADECB, 0xD50ADA38, 0x0339C32A, 0xC6913667, 0x8DF9317C, 0xE0B12B4F, 0xF79E59B7, 0x43F5BB3A, 0xF2D519FF, + 0x27D9459C, 0xBF97222C, 0x15E6FC2A, 0x0F91FC71, 0x9B941525, 0xFAE59361, 0xCEB69CEB, 0xC2A86459, 0x12BAA8D1, + 0xB6C1075E, 0xE3056A0C, 0x10D25065, 0xCB03A442, 0xE0EC6E0E, 0x1698DB3B, 0x4C98A0BE, 0x3278E964, 0x9F1F9532, + 0xE0D392DF, 0xD3A0342B, 0x8971F21E, 0x1B0A7441, 0x4BA3348C, 0xC5BE7120, 0xC37632D8, 0xDF359F8D, 0x9B992F2E, + 0xE60B6F47, 0x0FE3F11D, 0xE54CDA54, 0x1EDAD891, 0xCE6279CF, 0xCD3E7E6F, 0x1618B166, 0xFD2C1D05, 0x848FD2C5, + 0xF6FB2299, 0xF523F357, 0xA6327623, 0x93A83531, 0x56CCCD02, 0xACF08162, 0x5A75EBB5, 0x6E163697, 0x88D273CC, + 0xDE966292, 0x81B949D0, 0x4C50901B, 0x71C65614, 0xE6C6C7BD, 0x327A140A, 0x45E1D006, 0xC3F27B9A, 0xC9AA53FD, + 0x62A80F00, 0xBB25BFE2, 0x35BDD2F6, 0x71126905, 0xB2040222, 0xB6CBCF7C, 0xCD769C2B, 0x53113EC0, 0x1640E3D3, + 0x38ABBD60, 0x2547ADF0, 0xBA38209C, 0xF746CE76, 0x77AFA1C5, 0x20756060, 0x85CBFE4E, 0x8AE88DD8, 0x7AAAF9B0, + 0x4CF9AA7E, 0x1948C25C, 0x02FB8A8C, 0x01C36AE4, 0xD6EBE1F9, 0x90D4F869, 0xA65CDEA0, 0x3F09252D, 0xC208E69F, + 0xB74E6132, 0xCE77E25B, 0x578FDFE3, 0x3AC372E6 }; + + // ==================================== + // Useful constants + // ==================================== + + private static final int ROUNDS = 16; + private static final int BLOCK_SIZE = 8; // bytes = 64 bits + private static final int SBOX_SK = 256; + private static final int P_SZ = ROUNDS + 2; + + private final int[] S0, S1, S2, S3; // the s-boxes + private final int[] P; // the p-array + + private boolean doEncrypt = false; + + private byte[] workingKey = null; + + public BlowFish() + { + S0 = new int[SBOX_SK]; + S1 = new int[SBOX_SK]; + S2 = new int[SBOX_SK]; + S3 = new int[SBOX_SK]; + P = new int[P_SZ]; + } + + /** + * initialise a Blowfish cipher. + * + * @param encrypting + * whether or not we are for encryption. + * @param key + * the key required to set up the cipher. + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + public void init(boolean encrypting, byte[] key) + { + this.doEncrypt = encrypting; + this.workingKey = key; + setKey(this.workingKey); + } + + public String getAlgorithmName() + { + return "Blowfish"; + } + + public final void transformBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (workingKey == null) + { + throw new IllegalStateException("Blowfish not initialised"); + } + + if (doEncrypt) + { + encryptBlock(in, inOff, out, outOff); + } + else + { + decryptBlock(in, inOff, out, outOff); + } + } + + public void reset() + { + } + + public int getBlockSize() + { + return BLOCK_SIZE; + } + + // ================================== + // Private Implementation + // ================================== + + private int F(int x) + { + return (((S0[(x >>> 24)] + S1[(x >>> 16) & 0xff]) ^ S2[(x >>> 8) & 0xff]) + S3[x & 0xff]); + } + + /** + * apply the encryption cycle to each value pair in the table. + */ + private void processTable(int xl, int xr, int[] table) + { + int size = table.length; + + for (int s = 0; s < size; s += 2) + { + xl ^= P[0]; + + for (int i = 1; i < ROUNDS; i += 2) + { + xr ^= F(xl) ^ P[i]; + xl ^= F(xr) ^ P[i + 1]; + } + + xr ^= P[ROUNDS + 1]; + + table[s] = xr; + table[s + 1] = xl; + + xr = xl; // end of cycle swap + xl = table[s]; + } + } + + private void setKey(byte[] key) + { + /* + * - comments are from _Applied Crypto_, Schneier, p338 please be + * careful comparing the two, AC numbers the arrays from 1, the enclosed + * code from 0. + * + * (1) Initialise the S-boxes and the P-array, with a fixed string This + * string contains the hexadecimal digits of pi (3.141...) + */ + System.arraycopy(KS0, 0, S0, 0, SBOX_SK); + System.arraycopy(KS1, 0, S1, 0, SBOX_SK); + System.arraycopy(KS2, 0, S2, 0, SBOX_SK); + System.arraycopy(KS3, 0, S3, 0, SBOX_SK); + + System.arraycopy(KP, 0, P, 0, P_SZ); + + /* + * (2) Now, XOR P[0] with the first 32 bits of the key, XOR P[1] with + * the second 32-bits of the key, and so on for all bits of the key (up + * to P[17]). Repeatedly cycle through the key bits until the entire + * P-array has been XOR-ed with the key bits + */ + int keyLength = key.length; + int keyIndex = 0; + + for (int i = 0; i < P_SZ; i++) + { + // get the 32 bits of the key, in 4 * 8 bit chunks + int data = 0x0000000; + for (int j = 0; j < 4; j++) + { + // create a 32 bit block + data = (data << 8) | (key[keyIndex++] & 0xff); + + // wrap when we get to the end of the key + if (keyIndex >= keyLength) + { + keyIndex = 0; + } + } + // XOR the newly created 32 bit chunk onto the P-array + P[i] ^= data; + } + + /* + * (3) Encrypt the all-zero string with the Blowfish algorithm, using + * the subkeys described in (1) and (2) + * + * (4) Replace P1 and P2 with the output of step (3) + * + * (5) Encrypt the output of step(3) using the Blowfish algorithm, with + * the modified subkeys. + * + * (6) Replace P3 and P4 with the output of step (5) + * + * (7) Continue the process, replacing all elements of the P-array and + * then all four S-boxes in order, with the output of the continuously + * changing Blowfish algorithm + */ + + processTable(0, 0, P); + processTable(P[P_SZ - 2], P[P_SZ - 1], S0); + processTable(S0[SBOX_SK - 2], S0[SBOX_SK - 1], S1); + processTable(S1[SBOX_SK - 2], S1[SBOX_SK - 1], S2); + processTable(S2[SBOX_SK - 2], S2[SBOX_SK - 1], S3); + } + + /** + * Encrypt the given input starting at the given offset and place the result + * in the provided buffer starting at the given offset. The input will be an + * exact multiple of our blocksize. + */ + private void encryptBlock(byte[] src, int srcIndex, byte[] dst, int dstIndex) + { + int xl = BytesTo32bits(src, srcIndex); + int xr = BytesTo32bits(src, srcIndex + 4); + + xl ^= P[0]; + + for (int i = 1; i < ROUNDS; i += 2) + { + xr ^= F(xl) ^ P[i]; + xl ^= F(xr) ^ P[i + 1]; + } + + xr ^= P[ROUNDS + 1]; + + Bits32ToBytes(xr, dst, dstIndex); + Bits32ToBytes(xl, dst, dstIndex + 4); + } + + /** + * Decrypt the given input starting at the given offset and place the result + * in the provided buffer starting at the given offset. The input will be an + * exact multiple of our blocksize. + */ + private void decryptBlock(byte[] src, int srcIndex, byte[] dst, int dstIndex) + { + int xl = BytesTo32bits(src, srcIndex); + int xr = BytesTo32bits(src, srcIndex + 4); + + xl ^= P[ROUNDS + 1]; + + for (int i = ROUNDS; i > 0; i -= 2) + { + xr ^= F(xl) ^ P[i]; + xl ^= F(xr) ^ P[i - 1]; + } + + xr ^= P[0]; + + Bits32ToBytes(xr, dst, dstIndex); + Bits32ToBytes(xl, dst, dstIndex + 4); + } + + private int BytesTo32bits(byte[] b, int i) + { + return ((b[i] & 0xff) << 24) | ((b[i + 1] & 0xff) << 16) | ((b[i + 2] & 0xff) << 8) | ((b[i + 3] & 0xff)); + } + + private void Bits32ToBytes(int in, byte[] b, int offset) + { + b[offset + 3] = (byte) in; + b[offset + 2] = (byte) (in >> 8); + b[offset + 1] = (byte) (in >> 16); + b[offset] = (byte) (in >> 24); + } + +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java new file mode 100644 index 0000000..0ae51b3 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java @@ -0,0 +1,78 @@ +package com.trilead.ssh2.crypto.cipher; + +/** + * CBCMode. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CBCMode.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CBCMode implements BlockCipher +{ + BlockCipher tc; + int blockSize; + boolean doEncrypt; + + byte[] cbc_vector; + byte[] tmp_vector; + + public void init(boolean forEncryption, byte[] key) + { + } + + public CBCMode(BlockCipher tc, byte[] iv, boolean doEncrypt) + throws IllegalArgumentException + { + this.tc = tc; + this.blockSize = tc.getBlockSize(); + this.doEncrypt = doEncrypt; + + if (this.blockSize != iv.length) + throw new IllegalArgumentException("IV must be " + blockSize + + " bytes long! (currently " + iv.length + ")"); + + this.cbc_vector = new byte[blockSize]; + this.tmp_vector = new byte[blockSize]; + System.arraycopy(iv, 0, cbc_vector, 0, blockSize); + } + + public int getBlockSize() + { + return blockSize; + } + + private void encryptBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + for (int i = 0; i < blockSize; i++) + cbc_vector[i] ^= src[srcoff + i]; + + tc.transformBlock(cbc_vector, 0, dst, dstoff); + + System.arraycopy(dst, dstoff, cbc_vector, 0, blockSize); + } + + private void decryptBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + /* Assume the worst, src and dst are overlapping... */ + + System.arraycopy(src, srcoff, tmp_vector, 0, blockSize); + + tc.transformBlock(src, srcoff, dst, dstoff); + + for (int i = 0; i < blockSize; i++) + dst[dstoff + i] ^= cbc_vector[i]; + + /* ...that is why we need a tmp buffer. */ + + byte[] swap = cbc_vector; + cbc_vector = tmp_vector; + tmp_vector = swap; + } + + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + if (doEncrypt) + encryptBlock(src, srcoff, dst, dstoff); + else + decryptBlock(src, srcoff, dst, dstoff); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java new file mode 100644 index 0000000..8541c8d --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java @@ -0,0 +1,62 @@ + +package com.trilead.ssh2.crypto.cipher; + +/** + * This is CTR mode as described in draft-ietf-secsh-newmodes-XY.txt + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CTRMode.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CTRMode implements BlockCipher +{ + byte[] X; + byte[] Xenc; + + BlockCipher bc; + int blockSize; + boolean doEncrypt; + + int count = 0; + + public void init(boolean forEncryption, byte[] key) + { + } + + public CTRMode(BlockCipher tc, byte[] iv, boolean doEnc) throws IllegalArgumentException + { + bc = tc; + blockSize = bc.getBlockSize(); + doEncrypt = doEnc; + + if (blockSize != iv.length) + throw new IllegalArgumentException("IV must be " + blockSize + " bytes long! (currently " + iv.length + ")"); + + X = new byte[blockSize]; + Xenc = new byte[blockSize]; + + System.arraycopy(iv, 0, X, 0, blockSize); + } + + public final int getBlockSize() + { + return blockSize; + } + + public final void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + bc.transformBlock(X, 0, Xenc, 0); + + for (int i = 0; i < blockSize; i++) + { + dst[dstoff + i] = (byte) (src[srcoff + i] ^ Xenc[i]); + } + + for (int i = (blockSize - 1); i >= 0; i--) + { + X[i]++; + if (X[i] != 0) + break; + + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java new file mode 100644 index 0000000..c9055ab --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java @@ -0,0 +1,144 @@ + +package com.trilead.ssh2.crypto.cipher; + +import java.io.IOException; +import java.io.InputStream; + +/** + * CipherInputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CipherInputStream.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CipherInputStream +{ + BlockCipher currentCipher; + InputStream bi; + byte[] buffer; + byte[] enc; + int blockSize; + int pos; + + /* + * We cannot use java.io.BufferedInputStream, since that is not available in + * J2ME. Everything could be improved alot here. + */ + + final int BUFF_SIZE = 2048; + byte[] input_buffer = new byte[BUFF_SIZE]; + int input_buffer_pos = 0; + int input_buffer_size = 0; + + public CipherInputStream(BlockCipher tc, InputStream bi) + { + this.bi = bi; + changeCipher(tc); + } + + private int fill_buffer() throws IOException + { + input_buffer_pos = 0; + input_buffer_size = bi.read(input_buffer, 0, BUFF_SIZE); + return input_buffer_size; + } + + private int internal_read(byte[] b, int off, int len) throws IOException + { + if (input_buffer_size < 0) + return -1; + + if (input_buffer_pos >= input_buffer_size) + { + if (fill_buffer() <= 0) + return -1; + } + + int avail = input_buffer_size - input_buffer_pos; + int thiscopy = (len > avail) ? avail : len; + + System.arraycopy(input_buffer, input_buffer_pos, b, off, thiscopy); + input_buffer_pos += thiscopy; + + return thiscopy; + } + + public void changeCipher(BlockCipher bc) + { + this.currentCipher = bc; + blockSize = bc.getBlockSize(); + buffer = new byte[blockSize]; + enc = new byte[blockSize]; + pos = blockSize; + } + + private void getBlock() throws IOException + { + int n = 0; + while (n < blockSize) + { + int len = internal_read(enc, n, blockSize - n); + if (len < 0) + throw new IOException("Cannot read full block, EOF reached."); + n += len; + } + + try + { + currentCipher.transformBlock(enc, 0, buffer, 0); + } + catch (Exception e) + { + throw new IOException("Error while decrypting block."); + } + pos = 0; + } + + public int read(byte[] dst) throws IOException + { + return read(dst, 0, dst.length); + } + + public int read(byte[] dst, int off, int len) throws IOException + { + int count = 0; + + while (len > 0) + { + if (pos >= blockSize) + getBlock(); + + int avail = blockSize - pos; + int copy = Math.min(avail, len); + System.arraycopy(buffer, pos, dst, off, copy); + pos += copy; + off += copy; + len -= copy; + count += copy; + } + return count; + } + + public int read() throws IOException + { + if (pos >= blockSize) + { + getBlock(); + } + return buffer[pos++] & 0xff; + } + + public int readPlain(byte[] b, int off, int len) throws IOException + { + if (pos != blockSize) + throw new IOException("Cannot read plain since crypto buffer is not aligned."); + int n = 0; + while (n < len) + { + int cnt = internal_read(b, off + n, len - n); + if (cnt < 0) + throw new IOException("Cannot fill buffer, EOF reached."); + n += cnt; + } + return n; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java new file mode 100644 index 0000000..cf0db4a --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java @@ -0,0 +1,142 @@ + +package com.trilead.ssh2.crypto.cipher; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * CipherOutputStream. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: CipherOutputStream.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class CipherOutputStream +{ + BlockCipher currentCipher; + OutputStream bo; + byte[] buffer; + byte[] enc; + int blockSize; + int pos; + + /* + * We cannot use java.io.BufferedOutputStream, since that is not available + * in J2ME. Everything could be improved here alot. + */ + + final int BUFF_SIZE = 2048; + byte[] out_buffer = new byte[BUFF_SIZE]; + int out_buffer_pos = 0; + + public CipherOutputStream(BlockCipher tc, OutputStream bo) + { + this.bo = bo; + changeCipher(tc); + } + + private void internal_write(byte[] src, int off, int len) throws IOException + { + while (len > 0) + { + int space = BUFF_SIZE - out_buffer_pos; + int copy = (len > space) ? space : len; + + System.arraycopy(src, off, out_buffer, out_buffer_pos, copy); + + off += copy; + out_buffer_pos += copy; + len -= copy; + + if (out_buffer_pos >= BUFF_SIZE) + { + bo.write(out_buffer, 0, BUFF_SIZE); + out_buffer_pos = 0; + } + } + } + + private void internal_write(int b) throws IOException + { + out_buffer[out_buffer_pos++] = (byte) b; + if (out_buffer_pos >= BUFF_SIZE) + { + bo.write(out_buffer, 0, BUFF_SIZE); + out_buffer_pos = 0; + } + } + + public void flush() throws IOException + { + if (pos != 0) + throw new IOException("FATAL: cannot flush since crypto buffer is not aligned."); + + if (out_buffer_pos > 0) + { + bo.write(out_buffer, 0, out_buffer_pos); + out_buffer_pos = 0; + } + bo.flush(); + } + + public void changeCipher(BlockCipher bc) + { + this.currentCipher = bc; + blockSize = bc.getBlockSize(); + buffer = new byte[blockSize]; + enc = new byte[blockSize]; + pos = 0; + } + + private void writeBlock() throws IOException + { + try + { + currentCipher.transformBlock(buffer, 0, enc, 0); + } + catch (Exception e) + { + throw (IOException) new IOException("Error while decrypting block.").initCause(e); + } + + internal_write(enc, 0, blockSize); + pos = 0; + } + + public void write(byte[] src, int off, int len) throws IOException + { + while (len > 0) + { + int avail = blockSize - pos; + int copy = Math.min(avail, len); + + System.arraycopy(src, off, buffer, pos, copy); + pos += copy; + off += copy; + len -= copy; + + if (pos >= blockSize) + writeBlock(); + } + } + + public void write(int b) throws IOException + { + buffer[pos++] = (byte) b; + if (pos >= blockSize) + writeBlock(); + } + + public void writePlain(int b) throws IOException + { + if (pos != 0) + throw new IOException("Cannot write plain since crypto buffer is not aligned."); + internal_write(b); + } + + public void writePlain(byte[] b, int off, int len) throws IOException + { + if (pos != 0) + throw new IOException("Cannot write plain since crypto buffer is not aligned."); + internal_write(b, off, len); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java new file mode 100644 index 0000000..6588459 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java @@ -0,0 +1,373 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + This file is based on the 3DES implementation from the Bouncy Castle Crypto package. + Their licence file states the following: + + Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + (http://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + * DES. + * + * @author See comments in the source file + * @version $Id: DES.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class DES implements BlockCipher +{ + private int[] workingKey = null; + + /** + * standard constructor. + */ + public DES() + { + } + + /** + * initialise a DES cipher. + * + * @param encrypting + * whether or not we are for encryption. + * @param key + * the parameters required to set up the cipher. + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + public void init(boolean encrypting, byte[] key) + { + this.workingKey = generateWorkingKey(encrypting, key, 0); + } + + public String getAlgorithmName() + { + return "DES"; + } + + public int getBlockSize() + { + return 8; + } + + public void transformBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (workingKey == null) + { + throw new IllegalStateException("DES engine not initialised!"); + } + + desFunc(workingKey, in, inOff, out, outOff); + } + + public void reset() + { + } + + /** + * what follows is mainly taken from "Applied Cryptography", by Bruce + * Schneier, however it also bears great resemblance to Richard + * Outerbridge's D3DES... + */ + + static short[] Df_Key = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, + 0x10, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67 }; + + static short[] bytebit = { 0200, 0100, 040, 020, 010, 04, 02, 01 }; + + static int[] bigbyte = { 0x800000, 0x400000, 0x200000, 0x100000, 0x80000, 0x40000, 0x20000, 0x10000, 0x8000, + 0x4000, 0x2000, 0x1000, 0x800, 0x400, 0x200, 0x100, 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1 }; + + /* + * Use the key schedule specified in the Standard (ANSI X3.92-1981). + */ + + static byte[] pc1 = { 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, + 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, + 4, 27, 19, 11, 3 }; + + static byte[] totrot = { 1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28 }; + + static byte[] pc2 = { 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, + 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31 }; + + static int[] SP1 = { 0x01010400, 0x00000000, 0x00010000, 0x01010404, 0x01010004, 0x00010404, 0x00000004, + 0x00010000, 0x00000400, 0x01010400, 0x01010404, 0x00000400, 0x01000404, 0x01010004, 0x01000000, 0x00000004, + 0x00000404, 0x01000400, 0x01000400, 0x00010400, 0x00010400, 0x01010000, 0x01010000, 0x01000404, 0x00010004, + 0x01000004, 0x01000004, 0x00010004, 0x00000000, 0x00000404, 0x00010404, 0x01000000, 0x00010000, 0x01010404, + 0x00000004, 0x01010000, 0x01010400, 0x01000000, 0x01000000, 0x00000400, 0x01010004, 0x00010000, 0x00010400, + 0x01000004, 0x00000400, 0x00000004, 0x01000404, 0x00010404, 0x01010404, 0x00010004, 0x01010000, 0x01000404, + 0x01000004, 0x00000404, 0x00010404, 0x01010400, 0x00000404, 0x01000400, 0x01000400, 0x00000000, 0x00010004, + 0x00010400, 0x00000000, 0x01010004 }; + + static int[] SP2 = { 0x80108020, 0x80008000, 0x00008000, 0x00108020, 0x00100000, 0x00000020, 0x80100020, + 0x80008020, 0x80000020, 0x80108020, 0x80108000, 0x80000000, 0x80008000, 0x00100000, 0x00000020, 0x80100020, + 0x00108000, 0x00100020, 0x80008020, 0x00000000, 0x80000000, 0x00008000, 0x00108020, 0x80100000, 0x00100020, + 0x80000020, 0x00000000, 0x00108000, 0x00008020, 0x80108000, 0x80100000, 0x00008020, 0x00000000, 0x00108020, + 0x80100020, 0x00100000, 0x80008020, 0x80100000, 0x80108000, 0x00008000, 0x80100000, 0x80008000, 0x00000020, + 0x80108020, 0x00108020, 0x00000020, 0x00008000, 0x80000000, 0x00008020, 0x80108000, 0x00100000, 0x80000020, + 0x00100020, 0x80008020, 0x80000020, 0x00100020, 0x00108000, 0x00000000, 0x80008000, 0x00008020, 0x80000000, + 0x80100020, 0x80108020, 0x00108000 }; + + static int[] SP3 = { 0x00000208, 0x08020200, 0x00000000, 0x08020008, 0x08000200, 0x00000000, 0x00020208, + 0x08000200, 0x00020008, 0x08000008, 0x08000008, 0x00020000, 0x08020208, 0x00020008, 0x08020000, 0x00000208, + 0x08000000, 0x00000008, 0x08020200, 0x00000200, 0x00020200, 0x08020000, 0x08020008, 0x00020208, 0x08000208, + 0x00020200, 0x00020000, 0x08000208, 0x00000008, 0x08020208, 0x00000200, 0x08000000, 0x08020200, 0x08000000, + 0x00020008, 0x00000208, 0x00020000, 0x08020200, 0x08000200, 0x00000000, 0x00000200, 0x00020008, 0x08020208, + 0x08000200, 0x08000008, 0x00000200, 0x00000000, 0x08020008, 0x08000208, 0x00020000, 0x08000000, 0x08020208, + 0x00000008, 0x00020208, 0x00020200, 0x08000008, 0x08020000, 0x08000208, 0x00000208, 0x08020000, 0x00020208, + 0x00000008, 0x08020008, 0x00020200 }; + + static int[] SP4 = { 0x00802001, 0x00002081, 0x00002081, 0x00000080, 0x00802080, 0x00800081, 0x00800001, + 0x00002001, 0x00000000, 0x00802000, 0x00802000, 0x00802081, 0x00000081, 0x00000000, 0x00800080, 0x00800001, + 0x00000001, 0x00002000, 0x00800000, 0x00802001, 0x00000080, 0x00800000, 0x00002001, 0x00002080, 0x00800081, + 0x00000001, 0x00002080, 0x00800080, 0x00002000, 0x00802080, 0x00802081, 0x00000081, 0x00800080, 0x00800001, + 0x00802000, 0x00802081, 0x00000081, 0x00000000, 0x00000000, 0x00802000, 0x00002080, 0x00800080, 0x00800081, + 0x00000001, 0x00802001, 0x00002081, 0x00002081, 0x00000080, 0x00802081, 0x00000081, 0x00000001, 0x00002000, + 0x00800001, 0x00002001, 0x00802080, 0x00800081, 0x00002001, 0x00002080, 0x00800000, 0x00802001, 0x00000080, + 0x00800000, 0x00002000, 0x00802080 }; + + static int[] SP5 = { 0x00000100, 0x02080100, 0x02080000, 0x42000100, 0x00080000, 0x00000100, 0x40000000, + 0x02080000, 0x40080100, 0x00080000, 0x02000100, 0x40080100, 0x42000100, 0x42080000, 0x00080100, 0x40000000, + 0x02000000, 0x40080000, 0x40080000, 0x00000000, 0x40000100, 0x42080100, 0x42080100, 0x02000100, 0x42080000, + 0x40000100, 0x00000000, 0x42000000, 0x02080100, 0x02000000, 0x42000000, 0x00080100, 0x00080000, 0x42000100, + 0x00000100, 0x02000000, 0x40000000, 0x02080000, 0x42000100, 0x40080100, 0x02000100, 0x40000000, 0x42080000, + 0x02080100, 0x40080100, 0x00000100, 0x02000000, 0x42080000, 0x42080100, 0x00080100, 0x42000000, 0x42080100, + 0x02080000, 0x00000000, 0x40080000, 0x42000000, 0x00080100, 0x02000100, 0x40000100, 0x00080000, 0x00000000, + 0x40080000, 0x02080100, 0x40000100 }; + + static int[] SP6 = { 0x20000010, 0x20400000, 0x00004000, 0x20404010, 0x20400000, 0x00000010, 0x20404010, + 0x00400000, 0x20004000, 0x00404010, 0x00400000, 0x20000010, 0x00400010, 0x20004000, 0x20000000, 0x00004010, + 0x00000000, 0x00400010, 0x20004010, 0x00004000, 0x00404000, 0x20004010, 0x00000010, 0x20400010, 0x20400010, + 0x00000000, 0x00404010, 0x20404000, 0x00004010, 0x00404000, 0x20404000, 0x20000000, 0x20004000, 0x00000010, + 0x20400010, 0x00404000, 0x20404010, 0x00400000, 0x00004010, 0x20000010, 0x00400000, 0x20004000, 0x20000000, + 0x00004010, 0x20000010, 0x20404010, 0x00404000, 0x20400000, 0x00404010, 0x20404000, 0x00000000, 0x20400010, + 0x00000010, 0x00004000, 0x20400000, 0x00404010, 0x00004000, 0x00400010, 0x20004010, 0x00000000, 0x20404000, + 0x20000000, 0x00400010, 0x20004010 }; + + static int[] SP7 = { 0x00200000, 0x04200002, 0x04000802, 0x00000000, 0x00000800, 0x04000802, 0x00200802, + 0x04200800, 0x04200802, 0x00200000, 0x00000000, 0x04000002, 0x00000002, 0x04000000, 0x04200002, 0x00000802, + 0x04000800, 0x00200802, 0x00200002, 0x04000800, 0x04000002, 0x04200000, 0x04200800, 0x00200002, 0x04200000, + 0x00000800, 0x00000802, 0x04200802, 0x00200800, 0x00000002, 0x04000000, 0x00200800, 0x04000000, 0x00200800, + 0x00200000, 0x04000802, 0x04000802, 0x04200002, 0x04200002, 0x00000002, 0x00200002, 0x04000000, 0x04000800, + 0x00200000, 0x04200800, 0x00000802, 0x00200802, 0x04200800, 0x00000802, 0x04000002, 0x04200802, 0x04200000, + 0x00200800, 0x00000000, 0x00000002, 0x04200802, 0x00000000, 0x00200802, 0x04200000, 0x00000800, 0x04000002, + 0x04000800, 0x00000800, 0x00200002 }; + + static int[] SP8 = { 0x10001040, 0x00001000, 0x00040000, 0x10041040, 0x10000000, 0x10001040, 0x00000040, + 0x10000000, 0x00040040, 0x10040000, 0x10041040, 0x00041000, 0x10041000, 0x00041040, 0x00001000, 0x00000040, + 0x10040000, 0x10000040, 0x10001000, 0x00001040, 0x00041000, 0x00040040, 0x10040040, 0x10041000, 0x00001040, + 0x00000000, 0x00000000, 0x10040040, 0x10000040, 0x10001000, 0x00041040, 0x00040000, 0x00041040, 0x00040000, + 0x10041000, 0x00001000, 0x00000040, 0x10040040, 0x00001000, 0x00041040, 0x10001000, 0x00000040, 0x10000040, + 0x10040000, 0x10040040, 0x10000000, 0x00040000, 0x10001040, 0x00000000, 0x10041040, 0x00040040, 0x10000040, + 0x10040000, 0x10001000, 0x10001040, 0x00000000, 0x10041040, 0x00041000, 0x00041000, 0x00001040, 0x00001040, + 0x00040040, 0x10000000, 0x10041000 }; + + /** + * generate an integer based working key based on our secret key and what we + * processing we are planning to do. + * + * Acknowledgements for this routine go to James Gillogly & Phil Karn. + * (whoever, and wherever they are!). + */ + protected int[] generateWorkingKey(boolean encrypting, byte[] key, int off) + { + int[] newKey = new int[32]; + boolean[] pc1m = new boolean[56], pcr = new boolean[56]; + + for (int j = 0; j < 56; j++) + { + int l = pc1[j]; + + pc1m[j] = ((key[off + (l >>> 3)] & bytebit[l & 07]) != 0); + } + + for (int i = 0; i < 16; i++) + { + int l, m, n; + + if (encrypting) + { + m = i << 1; + } + else + { + m = (15 - i) << 1; + } + + n = m + 1; + newKey[m] = newKey[n] = 0; + + for (int j = 0; j < 28; j++) + { + l = j + totrot[i]; + if (l < 28) + { + pcr[j] = pc1m[l]; + } + else + { + pcr[j] = pc1m[l - 28]; + } + } + + for (int j = 28; j < 56; j++) + { + l = j + totrot[i]; + if (l < 56) + { + pcr[j] = pc1m[l]; + } + else + { + pcr[j] = pc1m[l - 28]; + } + } + + for (int j = 0; j < 24; j++) + { + if (pcr[pc2[j]]) + { + newKey[m] |= bigbyte[j]; + } + + if (pcr[pc2[j + 24]]) + { + newKey[n] |= bigbyte[j]; + } + } + } + + // + // store the processed key + // + for (int i = 0; i != 32; i += 2) + { + int i1, i2; + + i1 = newKey[i]; + i2 = newKey[i + 1]; + + newKey[i] = ((i1 & 0x00fc0000) << 6) | ((i1 & 0x00000fc0) << 10) | ((i2 & 0x00fc0000) >>> 10) + | ((i2 & 0x00000fc0) >>> 6); + + newKey[i + 1] = ((i1 & 0x0003f000) << 12) | ((i1 & 0x0000003f) << 16) | ((i2 & 0x0003f000) >>> 4) + | (i2 & 0x0000003f); + } + + return newKey; + } + + /** + * the DES engine. + */ + protected void desFunc(int[] wKey, byte[] in, int inOff, byte[] out, int outOff) + { + int work, right, left; + + left = (in[inOff + 0] & 0xff) << 24; + left |= (in[inOff + 1] & 0xff) << 16; + left |= (in[inOff + 2] & 0xff) << 8; + left |= (in[inOff + 3] & 0xff); + + right = (in[inOff + 4] & 0xff) << 24; + right |= (in[inOff + 5] & 0xff) << 16; + right |= (in[inOff + 6] & 0xff) << 8; + right |= (in[inOff + 7] & 0xff); + + work = ((left >>> 4) ^ right) & 0x0f0f0f0f; + right ^= work; + left ^= (work << 4); + work = ((left >>> 16) ^ right) & 0x0000ffff; + right ^= work; + left ^= (work << 16); + work = ((right >>> 2) ^ left) & 0x33333333; + left ^= work; + right ^= (work << 2); + work = ((right >>> 8) ^ left) & 0x00ff00ff; + left ^= work; + right ^= (work << 8); + right = ((right << 1) | ((right >>> 31) & 1)) & 0xffffffff; + work = (left ^ right) & 0xaaaaaaaa; + left ^= work; + right ^= work; + left = ((left << 1) | ((left >>> 31) & 1)) & 0xffffffff; + + for (int round = 0; round < 8; round++) + { + int fval; + + work = (right << 28) | (right >>> 4); + work ^= wKey[round * 4 + 0]; + fval = SP7[work & 0x3f]; + fval |= SP5[(work >>> 8) & 0x3f]; + fval |= SP3[(work >>> 16) & 0x3f]; + fval |= SP1[(work >>> 24) & 0x3f]; + work = right ^ wKey[round * 4 + 1]; + fval |= SP8[work & 0x3f]; + fval |= SP6[(work >>> 8) & 0x3f]; + fval |= SP4[(work >>> 16) & 0x3f]; + fval |= SP2[(work >>> 24) & 0x3f]; + left ^= fval; + work = (left << 28) | (left >>> 4); + work ^= wKey[round * 4 + 2]; + fval = SP7[work & 0x3f]; + fval |= SP5[(work >>> 8) & 0x3f]; + fval |= SP3[(work >>> 16) & 0x3f]; + fval |= SP1[(work >>> 24) & 0x3f]; + work = left ^ wKey[round * 4 + 3]; + fval |= SP8[work & 0x3f]; + fval |= SP6[(work >>> 8) & 0x3f]; + fval |= SP4[(work >>> 16) & 0x3f]; + fval |= SP2[(work >>> 24) & 0x3f]; + right ^= fval; + } + + right = (right << 31) | (right >>> 1); + work = (left ^ right) & 0xaaaaaaaa; + left ^= work; + right ^= work; + left = (left << 31) | (left >>> 1); + work = ((left >>> 8) ^ right) & 0x00ff00ff; + right ^= work; + left ^= (work << 8); + work = ((left >>> 2) ^ right) & 0x33333333; + right ^= work; + left ^= (work << 2); + work = ((right >>> 16) ^ left) & 0x0000ffff; + left ^= work; + right ^= (work << 16); + work = ((right >>> 4) ^ left) & 0x0f0f0f0f; + left ^= work; + right ^= (work << 4); + + out[outOff + 0] = (byte) ((right >>> 24) & 0xff); + out[outOff + 1] = (byte) ((right >>> 16) & 0xff); + out[outOff + 2] = (byte) ((right >>> 8) & 0xff); + out[outOff + 3] = (byte) (right & 0xff); + out[outOff + 4] = (byte) ((left >>> 24) & 0xff); + out[outOff + 5] = (byte) ((left >>> 16) & 0xff); + out[outOff + 6] = (byte) ((left >>> 8) & 0xff); + out[outOff + 7] = (byte) (left & 0xff); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java new file mode 100644 index 0000000..f47a636 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java @@ -0,0 +1,105 @@ + +package com.trilead.ssh2.crypto.cipher; + +/* + This file was shamelessly taken (and modified) from the Bouncy Castle Crypto package. + Their licence file states the following: + + Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + (http://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + * DESede. + * + * @author See comments in the source file + * @version $Id: DESede.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class DESede extends DES +{ + private int[] key1 = null; + private int[] key2 = null; + private int[] key3 = null; + + private boolean encrypt; + + /** + * standard constructor. + */ + public DESede() + { + } + + /** + * initialise a DES cipher. + * + * @param encrypting + * whether or not we are for encryption. + * @param key + * the parameters required to set up the cipher. + * @exception IllegalArgumentException + * if the params argument is inappropriate. + */ + public void init(boolean encrypting, byte[] key) + { + key1 = generateWorkingKey(encrypting, key, 0); + key2 = generateWorkingKey(!encrypting, key, 8); + key3 = generateWorkingKey(encrypting, key, 16); + + encrypt = encrypting; + } + + public String getAlgorithmName() + { + return "DESede"; + } + + public int getBlockSize() + { + return 8; + } + + public void transformBlock(byte[] in, int inOff, byte[] out, int outOff) + { + if (key1 == null) + { + throw new IllegalStateException("DESede engine not initialised!"); + } + + if (encrypt) + { + desFunc(key1, in, inOff, out, outOff); + desFunc(key2, out, outOff, out, outOff); + desFunc(key3, out, outOff, out, outOff); + } + else + { + desFunc(key3, in, inOff, out, outOff); + desFunc(key2, out, outOff, out, outOff); + desFunc(key1, out, outOff, out, outOff); + } + } + + public void reset() + { + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java b/app/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java new file mode 100644 index 0000000..38f8215 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java @@ -0,0 +1,35 @@ +package com.trilead.ssh2.crypto.cipher; + +/** + * NullCipher. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NullCipher.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class NullCipher implements BlockCipher +{ + private int blockSize = 8; + + public NullCipher() + { + } + + public NullCipher(int blockSize) + { + this.blockSize = blockSize; + } + + public void init(boolean forEncryption, byte[] key) + { + } + + public int getBlockSize() + { + return blockSize; + } + + public void transformBlock(byte[] src, int srcoff, byte[] dst, int dstoff) + { + System.arraycopy(src, srcoff, dst, dstoff, blockSize); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java b/app/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java new file mode 100644 index 0000000..3acde25 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java @@ -0,0 +1,132 @@ +/** + * + */ +package com.trilead.ssh2.crypto.dh; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.KeyAgreement; +import javax.crypto.interfaces.DHPrivateKey; +import javax.crypto.interfaces.DHPublicKey; +import javax.crypto.spec.DHParameterSpec; +import javax.crypto.spec.DHPublicKeySpec; + +/** + * @author kenny + * + */ +public class DhExchange extends GenericDhExchange { + + /* Given by the standard */ + + private static final BigInteger P1 = new BigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" + + "FFFFFFFFFFFFFFFF", 16); + + private static final BigInteger P14 = new BigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16); + + private static final BigInteger G = BigInteger.valueOf(2); + + /* Client public and private */ + + private DHPrivateKey clientPrivate; + private DHPublicKey clientPublic; + + /* Server public */ + + private DHPublicKey serverPublic; + + @Override + public void init(String name) throws IOException { + final DHParameterSpec spec; + if ("diffie-hellman-group1-sha1".equals(name)) { + spec = new DHParameterSpec(P1, G); + } else if ("diffie-hellman-group14-sha1".equals(name)) { + spec = new DHParameterSpec(P14, G); + } else { + throw new IllegalArgumentException("Unknown DH group " + name); + } + + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("DH"); + kpg.initialize(spec); + KeyPair pair = kpg.generateKeyPair(); + clientPrivate = (DHPrivateKey) pair.getPrivate(); + clientPublic = (DHPublicKey) pair.getPublic(); + } catch (NoSuchAlgorithmException e) { + throw (IOException) new IOException("No DH keypair generator").initCause(e); + } catch (InvalidAlgorithmParameterException e) { + throw (IOException) new IOException("Invalid DH parameters").initCause(e); + } + } + + @Override + public byte[] getE() { + if (clientPublic == null) + throw new IllegalStateException("DhExchange not initialized!"); + + return clientPublic.getY().toByteArray(); + } + + @Override + protected byte[] getServerE() { + if (serverPublic == null) + throw new IllegalStateException("DhExchange not initialized!"); + + return serverPublic.getY().toByteArray(); + } + + @Override + public void setF(byte[] f) throws IOException { + if (clientPublic == null) + throw new IllegalStateException("DhExchange not initialized!"); + + final KeyAgreement ka; + try { + KeyFactory kf = KeyFactory.getInstance("DH"); + DHParameterSpec params = clientPublic.getParams(); + this.serverPublic = (DHPublicKey) kf.generatePublic(new DHPublicKeySpec( + new BigInteger(f), params.getP(), params.getG())); + + ka = KeyAgreement.getInstance("DH"); + ka.init(clientPrivate); + ka.doPhase(serverPublic, true); + } catch (NoSuchAlgorithmException e) { + throw (IOException) new IOException("No DH key agreement method").initCause(e); + } catch (InvalidKeyException e) { + throw (IOException) new IOException("Invalid DH key").initCause(e); + } catch (InvalidKeySpecException e) { + throw (IOException) new IOException("Invalid DH key").initCause(e); + } + + sharedSecret = new BigInteger(ka.generateSecret()); + } + + @Override + public String getHashAlgo() { + return "SHA1"; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java b/app/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java new file mode 100644 index 0000000..a888950 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java @@ -0,0 +1,113 @@ + +package com.trilead.ssh2.crypto.dh; + +import java.math.BigInteger; +import java.security.SecureRandom; + +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.crypto.digest.HashForSSH2Types; + + +/** + * DhGroupExchange. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DhGroupExchange.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class DhGroupExchange +{ + /* Given by the standard */ + + private BigInteger p; + private BigInteger g; + + /* Client public and private */ + + private BigInteger e; + private BigInteger x; + + /* Server public */ + + private BigInteger f; + + /* Shared secret */ + + private BigInteger k; + + public DhGroupExchange(BigInteger p, BigInteger g) + { + this.p = p; + this.g = g; + } + + public void init(SecureRandom rnd) + { + k = null; + + x = new BigInteger(p.bitLength() - 1, rnd); + e = g.modPow(x, p); + } + + /** + * @return Returns the e. + */ + public BigInteger getE() + { + if (e == null) + throw new IllegalStateException("Not initialized!"); + + return e; + } + + /** + * @return Returns the shared secret k. + */ + public BigInteger getK() + { + if (k == null) + throw new IllegalStateException("Shared secret not yet known, need f first!"); + + return k; + } + + /** + * Sets f and calculates the shared secret. + */ + public void setF(BigInteger f) + { + if (e == null) + throw new IllegalStateException("Not initialized!"); + + BigInteger zero = BigInteger.valueOf(0); + + if (zero.compareTo(f) >= 0 || p.compareTo(f) <= 0) + throw new IllegalArgumentException("Invalid f specified!"); + + this.f = f; + this.k = f.modPow(x, p); + } + + public byte[] calculateH(String hashAlgo, byte[] clientversion, byte[] serverversion, + byte[] clientKexPayload, byte[] serverKexPayload, byte[] hostKey, DHGexParameters para) + { + HashForSSH2Types hash = new HashForSSH2Types(hashAlgo); + + hash.updateByteString(clientversion); + hash.updateByteString(serverversion); + hash.updateByteString(clientKexPayload); + hash.updateByteString(serverKexPayload); + hash.updateByteString(hostKey); + if (para.getMin_group_len() > 0) + hash.updateUINT32(para.getMin_group_len()); + hash.updateUINT32(para.getPref_group_len()); + if (para.getMax_group_len() > 0) + hash.updateUINT32(para.getMax_group_len()); + hash.updateBigInt(p); + hash.updateBigInt(g); + hash.updateBigInt(e); + hash.updateBigInt(f); + hash.updateBigInt(k); + + return hash.getDigest(); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java b/app/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java new file mode 100644 index 0000000..43d31ad --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java @@ -0,0 +1,106 @@ +/** + * + */ +package com.trilead.ssh2.crypto.dh; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.KeyAgreement; + +import com.trilead.ssh2.signature.ECDSASHA2Verify; + +/** + * @author kenny + * + */ +public class EcDhExchange extends GenericDhExchange { + private ECPrivateKey clientPrivate; + private ECPublicKey clientPublic; + private ECPublicKey serverPublic; + + @Override + public void init(String name) throws IOException { + final ECParameterSpec spec; + + if ("ecdh-sha2-nistp256".equals(name)) { + spec = ECDSASHA2Verify.EllipticCurves.nistp256; + } else if ("ecdh-sha2-nistp384".equals(name)) { + spec = ECDSASHA2Verify.EllipticCurves.nistp384; + } else if ("ecdh-sha2-nistp521".equals(name)) { + spec = ECDSASHA2Verify.EllipticCurves.nistp521; + } else { + throw new IllegalArgumentException("Unknown EC curve " + name); + } + + KeyPairGenerator kpg; + try { + kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(spec); + KeyPair pair = kpg.generateKeyPair(); + clientPrivate = (ECPrivateKey) pair.getPrivate(); + clientPublic = (ECPublicKey) pair.getPublic(); + } catch (NoSuchAlgorithmException e) { + throw (IOException) new IOException("No DH keypair generator").initCause(e); + } catch (InvalidAlgorithmParameterException e) { + throw (IOException) new IOException("Invalid DH parameters").initCause(e); + } + } + + @Override + public byte[] getE() { + return ECDSASHA2Verify.encodeECPoint(clientPublic.getW(), clientPublic.getParams() + .getCurve()); + } + + @Override + protected byte[] getServerE() { + return ECDSASHA2Verify.encodeECPoint(serverPublic.getW(), serverPublic.getParams() + .getCurve()); + } + + @Override + public void setF(byte[] f) throws IOException { + + if (clientPublic == null) + throw new IllegalStateException("DhDsaExchange not initialized!"); + + final KeyAgreement ka; + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + ECParameterSpec params = clientPublic.getParams(); + ECPoint serverPoint = ECDSASHA2Verify.decodeECPoint(f, params.getCurve()); + this.serverPublic = (ECPublicKey) kf.generatePublic(new ECPublicKeySpec(serverPoint, + params)); + + ka = KeyAgreement.getInstance("ECDH"); + ka.init(clientPrivate); + ka.doPhase(serverPublic, true); + } catch (NoSuchAlgorithmException e) { + throw (IOException) new IOException("No ECDH key agreement method").initCause(e); + } catch (InvalidKeyException e) { + throw (IOException) new IOException("Invalid ECDH key").initCause(e); + } catch (InvalidKeySpecException e) { + throw (IOException) new IOException("Invalid ECDH key").initCause(e); + } + + sharedSecret = new BigInteger(ka.generateSecret()); + } + + @Override + public String getHashAlgo() { + return ECDSASHA2Verify.getDigestAlgorithmForParams(clientPublic.getParams()); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java b/app/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java new file mode 100644 index 0000000..039ff75 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java @@ -0,0 +1,93 @@ + +package com.trilead.ssh2.crypto.dh; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +import com.trilead.ssh2.crypto.digest.HashForSSH2Types; +import com.trilead.ssh2.log.Logger; + + +/** + * DhExchange. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DhExchange.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public abstract class GenericDhExchange +{ + private static final Logger log = Logger.getLogger(GenericDhExchange.class); + + /* Shared secret */ + + BigInteger sharedSecret; + + protected GenericDhExchange() + { + } + + public static GenericDhExchange getInstance(String algo) { + if (algo.startsWith("ecdh-sha2-")) { + return new EcDhExchange(); + } else { + return new DhExchange(); + } + } + + public abstract void init(String name) throws IOException; + + /** + * @return Returns the e (public value) + * @throws IllegalStateException + */ + public abstract byte[] getE(); + + /** + * @return Returns the server's e (public value) + * @throws IllegalStateException + */ + protected abstract byte[] getServerE(); + + /** + * @return Returns the shared secret k. + * @throws IllegalStateException + */ + public BigInteger getK() + { + if (sharedSecret == null) + throw new IllegalStateException("Shared secret not yet known, need f first!"); + + return sharedSecret; + } + + /** + * @param f + */ + public abstract void setF(byte[] f) throws IOException; + + public byte[] calculateH(byte[] clientversion, byte[] serverversion, byte[] clientKexPayload, + byte[] serverKexPayload, byte[] hostKey) throws UnsupportedEncodingException + { + HashForSSH2Types hash = new HashForSSH2Types(getHashAlgo()); + + if (log.isEnabled()) + { + log.log(90, "Client: '" + new String(clientversion, "ISO-8859-1") + "'"); + log.log(90, "Server: '" + new String(serverversion, "ISO-8859-1") + "'"); + } + + hash.updateByteString(clientversion); + hash.updateByteString(serverversion); + hash.updateByteString(clientKexPayload); + hash.updateByteString(serverKexPayload); + hash.updateByteString(hostKey); + hash.updateByteString(getE()); + hash.updateByteString(getServerE()); + hash.updateBigInt(sharedSecret); + + return hash.getDigest(); + } + + public abstract String getHashAlgo(); +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java b/app/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java new file mode 100644 index 0000000..6b0d6e3 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java @@ -0,0 +1,91 @@ + +package com.trilead.ssh2.crypto.digest; + +import java.math.BigInteger; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * HashForSSH2Types. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: HashForSSH2Types.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class HashForSSH2Types +{ + MessageDigest md; + + public HashForSSH2Types(String type) + { + try { + md = MessageDigest.getInstance(type); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unsupported algorithm " + type); + } + } + + public void updateByte(byte b) + { + /* HACK - to test it with J2ME */ + byte[] tmp = new byte[1]; + tmp[0] = b; + md.update(tmp); + } + + public void updateBytes(byte[] b) + { + md.update(b); + } + + public void updateUINT32(int v) + { + md.update((byte) (v >> 24)); + md.update((byte) (v >> 16)); + md.update((byte) (v >> 8)); + md.update((byte) (v)); + } + + public void updateByteString(byte[] b) + { + updateUINT32(b.length); + updateBytes(b); + } + + public void updateBigInt(BigInteger b) + { + updateByteString(b.toByteArray()); + } + + public void reset() + { + md.reset(); + } + + public int getDigestLength() + { + return md.getDigestLength(); + } + + public byte[] getDigest() + { + byte[] tmp = new byte[md.getDigestLength()]; + getDigest(tmp); + return tmp; + } + + public void getDigest(byte[] out) + { + getDigest(out, 0); + } + + public void getDigest(byte[] out, int off) + { + try { + md.digest(out, off, out.length - off); + } catch (DigestException e) { + // TODO is this right?! + throw new RuntimeException("Unable to digest", e); + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java b/app/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java new file mode 100644 index 0000000..561599c --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java @@ -0,0 +1,157 @@ + +package com.trilead.ssh2.crypto.digest; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; + +/** + * MAC. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: MAC.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public final class MAC +{ + /** + * From http://tools.ietf.org/html/rfc4253 + */ + private static final String HMAC_MD5 = "hmac-md5"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + private static final String HMAC_MD5_96 = "hmac-md5-96"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + private static final String HMAC_SHA1 = "hmac-sha1"; + + /** + * From http://tools.ietf.org/html/rfc4253 + */ + private static final String HMAC_SHA1_96 = "hmac-sha1-96"; + + /** + * From http://tools.ietf.org/html/rfc6668 + */ + private static final String HMAC_SHA2_256 = "hmac-sha2-256"; + + /** + * From http://tools.ietf.org/html/rfc6668 + */ + private static final String HMAC_SHA2_512 = "hmac-sha2-512"; + + Mac mac; + int outSize; + int macSize; + byte[] buffer; + + /* Higher Priority First */ + private static final String[] MAC_LIST = { + HMAC_SHA2_256, HMAC_SHA2_512, + HMAC_SHA1_96, HMAC_SHA1, HMAC_MD5_96, HMAC_MD5 + }; + + public final static String[] getMacList() + { + return MAC_LIST; + } + + public final static void checkMacList(String[] macs) + { + for (int i = 0; i < macs.length; i++) + getKeyLen(macs[i]); + } + + public final static int getKeyLen(String type) + { + if (HMAC_SHA1.equals(type) || HMAC_SHA1_96.equals(type)) + return 20; + if (HMAC_MD5.equals(type) || HMAC_MD5_96.equals(type)) + return 16; + if (HMAC_SHA2_256.equals(type)) + return 32; + if (HMAC_SHA2_512.equals(type)) + return 64; + throw new IllegalArgumentException("Unkown algorithm " + type); + } + + public MAC(String type, byte[] key) + { + try { + if (HMAC_SHA1.equals(type) || HMAC_SHA1_96.equals(type)) + { + mac = Mac.getInstance("HmacSHA1"); + } + else if (HMAC_MD5.equals(type) || HMAC_MD5_96.equals(type)) + { + mac = Mac.getInstance("HmacMD5"); + } + else if (HMAC_SHA2_256.equals(type)) + { + mac = Mac.getInstance("HmacSHA256"); + } + else if (HMAC_SHA2_512.equals(type)) + { + mac = Mac.getInstance("HmacSHA512"); + } + else + throw new IllegalArgumentException("Unkown algorithm " + type); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unknown algorithm " + type, e); + } + + macSize = mac.getMacLength(); + if (type.endsWith("-96")) { + outSize = 12; + buffer = new byte[macSize]; + } else { + outSize = macSize; + buffer = null; + } + + try { + mac.init(new SecretKeySpec(key, type)); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(e); + } + } + + public final void initMac(int seq) + { + mac.reset(); + mac.update((byte) (seq >> 24)); + mac.update((byte) (seq >> 16)); + mac.update((byte) (seq >> 8)); + mac.update((byte) (seq)); + } + + public final void update(byte[] packetdata, int off, int len) + { + mac.update(packetdata, off, len); + } + + public final void getMac(byte[] out, int off) + { + try { + if (buffer != null) { + mac.doFinal(buffer, 0); + System.arraycopy(buffer, 0, out, off, out.length - off); + } else { + mac.doFinal(out, off); + } + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + } + + public final int size() + { + return outSize; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/log/Logger.java b/app/src/main/java/com/trilead/ssh2/log/Logger.java new file mode 100644 index 0000000..20ab397 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/log/Logger.java @@ -0,0 +1,54 @@ + +package com.trilead.ssh2.log; + +import com.trilead.ssh2.DebugLogger; + +/** + * Logger - a very simple logger, mainly used during development. + * Is not based on log4j (to reduce external dependencies). + * However, if needed, something like log4j could easily be + * hooked in. + *

+ * For speed reasons, the static variables are not protected + * with semaphores. In other words, if you dynamicaly change the + * logging settings, then some threads may still use the old setting. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Logger.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $ + */ + +public class Logger +{ + public static boolean enabled = false; + public static DebugLogger logger = null; + + private String className; + + public final static Logger getLogger(Class x) + { + return new Logger(x); + } + + public Logger(Class x) + { + this.className = x.getName(); + } + + public final boolean isEnabled() + { + return enabled; + } + + public final void log(int level, String message) + { + if (!enabled) + return; + + DebugLogger target = logger; + + if (target == null) + return; + + target.log(level, className, message); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java new file mode 100644 index 0000000..95fa396 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.packets; + +/** + * PacketGlobalAuthAgent. + * + * @author Kenny Root, kenny@the-b.org + * @version $Id$ + */ +public class PacketChannelAuthAgentReq +{ + byte[] payload; + + public int recipientChannelID; + + public PacketChannelAuthAgentReq(int recipientChannelID) + { + this.recipientChannelID = recipientChannelID; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("auth-agent-req@openssh.com"); + tw.writeBoolean(true); // want reply + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java new file mode 100644 index 0000000..bd2ea3f --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java @@ -0,0 +1,66 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketChannelOpenConfirmation. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelOpenConfirmation.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketChannelOpenConfirmation +{ + byte[] payload; + + public int recipientChannelID; + public int senderChannelID; + public int initialWindowSize; + public int maxPacketSize; + + public PacketChannelOpenConfirmation(int recipientChannelID, int senderChannelID, int initialWindowSize, + int maxPacketSize) + { + this.recipientChannelID = recipientChannelID; + this.senderChannelID = senderChannelID; + this.initialWindowSize = initialWindowSize; + this.maxPacketSize = maxPacketSize; + } + + public PacketChannelOpenConfirmation(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION) + throw new IOException( + "This is not a SSH_MSG_CHANNEL_OPEN_CONFIRMATION! (" + + packet_type + ")"); + + recipientChannelID = tr.readUINT32(); + senderChannelID = tr.readUINT32(); + initialWindowSize = tr.readUINT32(); + maxPacketSize = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_OPEN_CONFIRMATION packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION); + tw.writeUINT32(recipientChannelID); + tw.writeUINT32(senderChannelID); + tw.writeUINT32(initialWindowSize); + tw.writeUINT32(maxPacketSize); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java new file mode 100644 index 0000000..1370355 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java @@ -0,0 +1,66 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketChannelOpenFailure. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelOpenFailure.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketChannelOpenFailure +{ + byte[] payload; + + public int recipientChannelID; + public int reasonCode; + public String description; + public String languageTag; + + public PacketChannelOpenFailure(int recipientChannelID, int reasonCode, String description, + String languageTag) + { + this.recipientChannelID = recipientChannelID; + this.reasonCode = reasonCode; + this.description = description; + this.languageTag = languageTag; + } + + public PacketChannelOpenFailure(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_OPEN_FAILURE) + throw new IOException( + "This is not a SSH_MSG_CHANNEL_OPEN_FAILURE! (" + + packet_type + ")"); + + recipientChannelID = tr.readUINT32(); + reasonCode = tr.readUINT32(); + description = tr.readString(); + languageTag = tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_OPEN_FAILURE packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN_FAILURE); + tw.writeUINT32(recipientChannelID); + tw.writeUINT32(reasonCode); + tw.writeString(description); + tw.writeString(languageTag); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java new file mode 100644 index 0000000..c337930 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java @@ -0,0 +1,35 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketChannelTrileadPing. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelTrileadPing.java,v 1.1 2008/03/03 07:01:36 + * cplattne Exp $ + */ +public class PacketChannelTrileadPing +{ + byte[] payload; + + public int recipientChannelID; + + public PacketChannelTrileadPing(int recipientChannelID) + { + this.recipientChannelID = recipientChannelID; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("trilead-ping"); + tw.writeBoolean(true); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java new file mode 100644 index 0000000..37ec081 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java @@ -0,0 +1,57 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketChannelWindowAdjust. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketChannelWindowAdjust.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketChannelWindowAdjust +{ + byte[] payload; + + public int recipientChannelID; + public int windowChange; + + public PacketChannelWindowAdjust(int recipientChannelID, int windowChange) + { + this.recipientChannelID = recipientChannelID; + this.windowChange = windowChange; + } + + public PacketChannelWindowAdjust(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST) + throw new IOException( + "This is not a SSH_MSG_CHANNEL_WINDOW_ADJUST! (" + + packet_type + ")"); + + recipientChannelID = tr.readUINT32(); + windowChange = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_WINDOW_ADJUST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST); + tw.writeUINT32(recipientChannelID); + tw.writeUINT32(windowChange); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java b/app/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java new file mode 100644 index 0000000..50d6ec2 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java @@ -0,0 +1,57 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketDisconnect. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketDisconnect.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class PacketDisconnect +{ + byte[] payload; + + int reason; + String desc; + String lang; + + public PacketDisconnect(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_DISCONNECT) + throw new IOException("This is not a Disconnect Packet! (" + packet_type + ")"); + + reason = tr.readUINT32(); + desc = tr.readString(); + lang = tr.readString(); + } + + public PacketDisconnect(int reason, String desc, String lang) + { + this.reason = reason; + this.desc = desc; + this.lang = lang; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_DISCONNECT); + tw.writeUINT32(reason); + tw.writeString(desc); + tw.writeString(lang); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java new file mode 100644 index 0000000..20bd558 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java @@ -0,0 +1,42 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketGlobalCancelForwardRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketGlobalCancelForwardRequest.java,v 1.1 2007/10/15 12:49:55 + * cplattne Exp $ + */ +public class PacketGlobalCancelForwardRequest +{ + byte[] payload; + + public boolean wantReply; + public String bindAddress; + public int bindPort; + + public PacketGlobalCancelForwardRequest(boolean wantReply, String bindAddress, int bindPort) + { + this.wantReply = wantReply; + this.bindAddress = bindAddress; + this.bindPort = bindPort; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_GLOBAL_REQUEST); + + tw.writeString("cancel-tcpip-forward"); + tw.writeBoolean(wantReply); + tw.writeString(bindAddress); + tw.writeUINT32(bindPort); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java new file mode 100644 index 0000000..55257e9 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java @@ -0,0 +1,41 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketGlobalForwardRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketGlobalForwardRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketGlobalForwardRequest +{ + byte[] payload; + + public boolean wantReply; + public String bindAddress; + public int bindPort; + + public PacketGlobalForwardRequest(boolean wantReply, String bindAddress, int bindPort) + { + this.wantReply = wantReply; + this.bindAddress = bindAddress; + this.bindPort = bindPort; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_GLOBAL_REQUEST); + + tw.writeString("tcpip-forward"); + tw.writeBoolean(wantReply); + tw.writeString(bindAddress); + tw.writeUINT32(bindPort); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java b/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java new file mode 100644 index 0000000..3d8930e --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java @@ -0,0 +1,32 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketGlobalTrileadPing. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketGlobalTrileadPing.java,v 1.1 2008/03/03 07:01:36 cplattne Exp $ + */ +public class PacketGlobalTrileadPing +{ + byte[] payload; + + public PacketGlobalTrileadPing() + { + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_GLOBAL_REQUEST); + + tw.writeString("trilead-ping"); + tw.writeBoolean(true); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java b/app/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java new file mode 100644 index 0000000..2b4d917 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java @@ -0,0 +1,59 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketIgnore. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketIgnore.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketIgnore +{ + byte[] payload; + + byte[] data; + + public void setData(byte[] data) + { + this.data = data; + payload = null; + } + + public PacketIgnore() + { + } + + public PacketIgnore(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_IGNORE) + throw new IOException("This is not a SSH_MSG_IGNORE packet! (" + packet_type + ")"); + + /* Could parse String body */ + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_IGNORE); + + if (data != null) + tw.writeString(data, 0, data.length); + else + tw.writeString(""); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java new file mode 100644 index 0000000..0092516 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java @@ -0,0 +1,31 @@ +package com.trilead.ssh2.packets; + +/** + * PacketKexDHInit. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDHInit.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDHInit +{ + byte[] payload; + + byte[] publicKey; + + public PacketKexDHInit(byte[] publicKey) + { + this.publicKey = publicKey; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEXDH_INIT); + tw.writeString(publicKey, 0, publicKey.length); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java new file mode 100644 index 0000000..51f2bda --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java @@ -0,0 +1,53 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketKexDHReply. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDHReply.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDHReply +{ + byte[] payload; + + byte[] hostKey; + byte[] publicKey; + byte[] signature; + + public PacketKexDHReply(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEXDH_REPLY) + throw new IOException("This is not a SSH_MSG_KEXDH_REPLY! (" + + packet_type + ")"); + + hostKey = tr.readByteString(); + publicKey = tr.readByteString(); + signature = tr.readByteString(); + + if (tr.remain() != 0) throw new IOException("PADDING IN SSH_MSG_KEXDH_REPLY!"); + } + + public byte[] getF() + { + return publicKey; + } + + public byte[] getHostKey() + { + return hostKey; + } + + public byte[] getSignature() + { + return signature; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java new file mode 100644 index 0000000..db85b61 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java @@ -0,0 +1,50 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +import java.math.BigInteger; + +/** + * PacketKexDhGexGroup. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexGroup.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexGroup +{ + byte[] payload; + + BigInteger p; + BigInteger g; + + public PacketKexDhGexGroup(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEX_DH_GEX_GROUP) + throw new IllegalArgumentException( + "This is not a SSH_MSG_KEX_DH_GEX_GROUP! (" + packet_type + + ")"); + + p = tr.readMPINT(); + g = tr.readMPINT(); + + if (tr.remain() != 0) + throw new IOException("PADDING IN SSH_MSG_KEX_DH_GEX_GROUP!"); + } + + public BigInteger getG() + { + return g; + } + + public BigInteger getP() + { + return p; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java new file mode 100644 index 0000000..8b34230 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.packets; + +import java.math.BigInteger; + +/** + * PacketKexDhGexInit. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexInit.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexInit +{ + byte[] payload; + + BigInteger e; + + public PacketKexDhGexInit(BigInteger e) + { + this.e = e; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEX_DH_GEX_INIT); + tw.writeMPInt(e); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java new file mode 100644 index 0000000..382b3b7 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java @@ -0,0 +1,56 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +import java.math.BigInteger; + +/** + * PacketKexDhGexReply. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexReply.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexReply +{ + byte[] payload; + + byte[] hostKey; + BigInteger f; + byte[] signature; + + public PacketKexDhGexReply(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEX_DH_GEX_REPLY) + throw new IOException("This is not a SSH_MSG_KEX_DH_GEX_REPLY! (" + packet_type + ")"); + + hostKey = tr.readByteString(); + f = tr.readMPINT(); + signature = tr.readByteString(); + + if (tr.remain() != 0) + throw new IOException("PADDING IN SSH_MSG_KEX_DH_GEX_REPLY!"); + } + + public BigInteger getF() + { + return f; + } + + public byte[] getHostKey() + { + return hostKey; + } + + public byte[] getSignature() + { + return signature; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java new file mode 100644 index 0000000..50369df --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java @@ -0,0 +1,39 @@ +package com.trilead.ssh2.packets; + +import com.trilead.ssh2.DHGexParameters; + +/** + * PacketKexDhGexRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexRequest +{ + byte[] payload; + + int min; + int n; + int max; + + public PacketKexDhGexRequest(DHGexParameters para) + { + this.min = para.getMin_group_len(); + this.n = para.getPref_group_len(); + this.max = para.getMax_group_len(); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEX_DH_GEX_REQUEST); + tw.writeUINT32(min); + tw.writeUINT32(n); + tw.writeUINT32(max); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java new file mode 100644 index 0000000..327f379 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java @@ -0,0 +1,34 @@ + +package com.trilead.ssh2.packets; + +import com.trilead.ssh2.DHGexParameters; + +/** + * PacketKexDhGexRequestOld. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexDhGexRequestOld.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexDhGexRequestOld +{ + byte[] payload; + + int n; + + public PacketKexDhGexRequestOld(DHGexParameters para) + { + this.n = para.getPref_group_len(); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEX_DH_GEX_REQUEST_OLD); + tw.writeUINT32(n); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java b/app/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java new file mode 100644 index 0000000..087d658 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java @@ -0,0 +1,165 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.security.SecureRandom; + +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.transport.KexParameters; + + +/** + * PacketKexInit. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketKexInit.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketKexInit +{ + byte[] payload; + + KexParameters kp = new KexParameters(); + + public PacketKexInit(CryptoWishList cwl) + { + kp.cookie = new byte[16]; + new SecureRandom().nextBytes(kp.cookie); + + kp.kex_algorithms = cwl.kexAlgorithms; + kp.server_host_key_algorithms = cwl.serverHostKeyAlgorithms; + kp.encryption_algorithms_client_to_server = cwl.c2s_enc_algos; + kp.encryption_algorithms_server_to_client = cwl.s2c_enc_algos; + kp.mac_algorithms_client_to_server = cwl.c2s_mac_algos; + kp.mac_algorithms_server_to_client = cwl.s2c_mac_algos; + kp.compression_algorithms_client_to_server = cwl.c2s_comp_algos; + kp.compression_algorithms_server_to_client = cwl.s2c_comp_algos; + kp.languages_client_to_server = new String[] {}; + kp.languages_server_to_client = new String[] {}; + kp.first_kex_packet_follows = false; + kp.reserved_field1 = 0; + } + + public PacketKexInit(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_KEXINIT) + throw new IOException("This is not a KexInitPacket! (" + packet_type + ")"); + + kp.cookie = tr.readBytes(16); + kp.kex_algorithms = tr.readNameList(); + kp.server_host_key_algorithms = tr.readNameList(); + kp.encryption_algorithms_client_to_server = tr.readNameList(); + kp.encryption_algorithms_server_to_client = tr.readNameList(); + kp.mac_algorithms_client_to_server = tr.readNameList(); + kp.mac_algorithms_server_to_client = tr.readNameList(); + kp.compression_algorithms_client_to_server = tr.readNameList(); + kp.compression_algorithms_server_to_client = tr.readNameList(); + kp.languages_client_to_server = tr.readNameList(); + kp.languages_server_to_client = tr.readNameList(); + kp.first_kex_packet_follows = tr.readBoolean(); + kp.reserved_field1 = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in KexInitPacket!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_KEXINIT); + tw.writeBytes(kp.cookie, 0, 16); + tw.writeNameList(kp.kex_algorithms); + tw.writeNameList(kp.server_host_key_algorithms); + tw.writeNameList(kp.encryption_algorithms_client_to_server); + tw.writeNameList(kp.encryption_algorithms_server_to_client); + tw.writeNameList(kp.mac_algorithms_client_to_server); + tw.writeNameList(kp.mac_algorithms_server_to_client); + tw.writeNameList(kp.compression_algorithms_client_to_server); + tw.writeNameList(kp.compression_algorithms_server_to_client); + tw.writeNameList(kp.languages_client_to_server); + tw.writeNameList(kp.languages_server_to_client); + tw.writeBoolean(kp.first_kex_packet_follows); + tw.writeUINT32(kp.reserved_field1); + payload = tw.getBytes(); + } + return payload; + } + + public KexParameters getKexParameters() + { + return kp; + } + + public String[] getCompression_algorithms_client_to_server() + { + return kp.compression_algorithms_client_to_server; + } + + public String[] getCompression_algorithms_server_to_client() + { + return kp.compression_algorithms_server_to_client; + } + + public byte[] getCookie() + { + return kp.cookie; + } + + public String[] getEncryption_algorithms_client_to_server() + { + return kp.encryption_algorithms_client_to_server; + } + + public String[] getEncryption_algorithms_server_to_client() + { + return kp.encryption_algorithms_server_to_client; + } + + public boolean isFirst_kex_packet_follows() + { + return kp.first_kex_packet_follows; + } + + public String[] getKex_algorithms() + { + return kp.kex_algorithms; + } + + public String[] getLanguages_client_to_server() + { + return kp.languages_client_to_server; + } + + public String[] getLanguages_server_to_client() + { + return kp.languages_server_to_client; + } + + public String[] getMac_algorithms_client_to_server() + { + return kp.mac_algorithms_client_to_server; + } + + public String[] getMac_algorithms_server_to_client() + { + return kp.mac_algorithms_server_to_client; + } + + public int getReserved_field1() + { + return kp.reserved_field1; + } + + public String[] getServer_host_key_algorithms() + { + return kp.server_host_key_algorithms; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java b/app/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java new file mode 100644 index 0000000..3ca6503 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java @@ -0,0 +1,46 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketNewKeys. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketNewKeys.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketNewKeys +{ + byte[] payload; + + public PacketNewKeys() + { + } + + public PacketNewKeys(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_NEWKEYS) + throw new IOException("This is not a SSH_MSG_NEWKEYS! (" + + packet_type + ")"); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_NEWKEYS packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_NEWKEYS); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java b/app/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java new file mode 100644 index 0000000..da6cbef --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java @@ -0,0 +1,56 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketOpenDirectTCPIPChannel. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketOpenDirectTCPIPChannel.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketOpenDirectTCPIPChannel +{ + byte[] payload; + + int channelID; + int initialWindowSize; + int maxPacketSize; + + String host_to_connect; + int port_to_connect; + String originator_IP_address; + int originator_port; + + public PacketOpenDirectTCPIPChannel(int channelID, int initialWindowSize, int maxPacketSize, + String host_to_connect, int port_to_connect, String originator_IP_address, + int originator_port) + { + this.channelID = channelID; + this.initialWindowSize = initialWindowSize; + this.maxPacketSize = maxPacketSize; + this.host_to_connect = host_to_connect; + this.port_to_connect = port_to_connect; + this.originator_IP_address = originator_IP_address; + this.originator_port = originator_port; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN); + tw.writeString("direct-tcpip"); + tw.writeUINT32(channelID); + tw.writeUINT32(initialWindowSize); + tw.writeUINT32(maxPacketSize); + tw.writeString(host_to_connect); + tw.writeUINT32(port_to_connect); + tw.writeString(originator_IP_address); + tw.writeUINT32(originator_port); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java b/app/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java new file mode 100644 index 0000000..a75ea63 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java @@ -0,0 +1,62 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketOpenSessionChannel. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketOpenSessionChannel.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketOpenSessionChannel +{ + byte[] payload; + + int channelID; + int initialWindowSize; + int maxPacketSize; + + public PacketOpenSessionChannel(int channelID, int initialWindowSize, + int maxPacketSize) + { + this.channelID = channelID; + this.initialWindowSize = initialWindowSize; + this.maxPacketSize = maxPacketSize; + } + + public PacketOpenSessionChannel(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_CHANNEL_OPEN) + throw new IOException("This is not a SSH_MSG_CHANNEL_OPEN! (" + + packet_type + ")"); + + channelID = tr.readUINT32(); + initialWindowSize = tr.readUINT32(); + maxPacketSize = tr.readUINT32(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_CHANNEL_OPEN packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_OPEN); + tw.writeString("session"); + tw.writeUINT32(channelID); + tw.writeUINT32(initialWindowSize); + tw.writeUINT32(maxPacketSize); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java b/app/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java new file mode 100644 index 0000000..d5c9a90 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java @@ -0,0 +1,61 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketServiceAccept. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketServiceAccept.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class PacketServiceAccept +{ + byte[] payload; + + String serviceName; + + public PacketServiceAccept(String serviceName) + { + this.serviceName = serviceName; + } + + public PacketServiceAccept(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_SERVICE_ACCEPT) + throw new IOException("This is not a SSH_MSG_SERVICE_ACCEPT! (" + packet_type + ")"); + + /* Be clever in case the server is not. Some servers seem to violate RFC4253 */ + + if (tr.remain() > 0) + { + serviceName = tr.readString(); + } + else + { + serviceName = ""; + } + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_SERVICE_ACCEPT packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_SERVICE_ACCEPT); + tw.writeString(serviceName); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java new file mode 100644 index 0000000..c2d2065 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java @@ -0,0 +1,52 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketServiceRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketServiceRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketServiceRequest +{ + byte[] payload; + + String serviceName; + + public PacketServiceRequest(String serviceName) + { + this.serviceName = serviceName; + } + + public PacketServiceRequest(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_SERVICE_REQUEST) + throw new IOException("This is not a SSH_MSG_SERVICE_REQUEST! (" + + packet_type + ")"); + + serviceName = tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_SERVICE_REQUEST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_SERVICE_REQUEST); + tw.writeString(serviceName); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java new file mode 100644 index 0000000..84efa5d --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java @@ -0,0 +1,39 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketSessionExecCommand. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionExecCommand.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionExecCommand +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String command; + + public PacketSessionExecCommand(int recipientChannelID, boolean wantReply, String command) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.command = command; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("exec"); + tw.writeBoolean(wantReply); + tw.writeString(command); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java new file mode 100644 index 0000000..d9c3d59 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java @@ -0,0 +1,57 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketSessionPtyRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionPtyRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionPtyRequest +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String term; + public int character_width; + public int character_height; + public int pixel_width; + public int pixel_height; + public byte[] terminal_modes; + + public PacketSessionPtyRequest(int recipientChannelID, boolean wantReply, String term, + int character_width, int character_height, int pixel_width, int pixel_height, + byte[] terminal_modes) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.term = term; + this.character_width = character_width; + this.character_height = character_height; + this.pixel_width = pixel_width; + this.pixel_height = pixel_height; + this.terminal_modes = terminal_modes; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("pty-req"); + tw.writeBoolean(wantReply); + tw.writeString(term); + tw.writeUINT32(character_width); + tw.writeUINT32(character_height); + tw.writeUINT32(pixel_width); + tw.writeUINT32(pixel_height); + tw.writeString(terminal_modes, 0, terminal_modes.length); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java new file mode 100644 index 0000000..1e3b558 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java @@ -0,0 +1,40 @@ +package com.trilead.ssh2.packets; + +public class PacketSessionPtyResize { + byte[] payload; + + public int recipientChannelID; + public int width; + public int height; + public int pixelWidth; + public int pixelHeight; + + public PacketSessionPtyResize(int recipientChannelID, int width, int height, int pixelWidth, int pixelHeight) { + this.recipientChannelID = recipientChannelID; + this.width = width; + this.height = height; + this.pixelWidth = pixelWidth; + this.pixelHeight = pixelHeight; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("window-change"); + tw.writeBoolean(false); + tw.writeUINT32(width); + tw.writeUINT32(height); + tw.writeUINT32(pixelWidth); + tw.writeUINT32(pixelHeight); + + payload = tw.getBytes(); + } + return payload; + } +} + + diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java new file mode 100644 index 0000000..e5add01 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java @@ -0,0 +1,36 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketSessionStartShell. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionStartShell.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionStartShell +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + + public PacketSessionStartShell(int recipientChannelID, boolean wantReply) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("shell"); + tw.writeBoolean(wantReply); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java new file mode 100644 index 0000000..cdc3a8c --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java @@ -0,0 +1,40 @@ +package com.trilead.ssh2.packets; + + +/** + * PacketSessionSubsystemRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionSubsystemRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionSubsystemRequest +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + public String subsystem; + + public PacketSessionSubsystemRequest(int recipientChannelID, boolean wantReply, String subsystem) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + this.subsystem = subsystem; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("subsystem"); + tw.writeBoolean(wantReply); + tw.writeString(subsystem); + payload = tw.getBytes(); + tw.getBytes(payload); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java new file mode 100644 index 0000000..26479c7 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java @@ -0,0 +1,53 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketSessionX11Request. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketSessionX11Request.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketSessionX11Request +{ + byte[] payload; + + public int recipientChannelID; + public boolean wantReply; + + public boolean singleConnection; + String x11AuthenticationProtocol; + String x11AuthenticationCookie; + int x11ScreenNumber; + + public PacketSessionX11Request(int recipientChannelID, boolean wantReply, boolean singleConnection, + String x11AuthenticationProtocol, String x11AuthenticationCookie, int x11ScreenNumber) + { + this.recipientChannelID = recipientChannelID; + this.wantReply = wantReply; + + this.singleConnection = singleConnection; + this.x11AuthenticationProtocol = x11AuthenticationProtocol; + this.x11AuthenticationCookie = x11AuthenticationCookie; + this.x11ScreenNumber = x11ScreenNumber; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("x11-req"); + tw.writeBoolean(wantReply); + + tw.writeBoolean(singleConnection); + tw.writeString(x11AuthenticationProtocol); + tw.writeString(x11AuthenticationCookie); + tw.writeUINT32(x11ScreenNumber); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java new file mode 100644 index 0000000..8ad8c3b --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java @@ -0,0 +1,60 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthBanner. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthBanner.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthBanner +{ + byte[] payload; + + String message; + String language; + + public PacketUserauthBanner(String message, String language) + { + this.message = message; + this.language = language; + } + + public String getBanner() + { + return message; + } + + public PacketUserauthBanner(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_BANNER) + throw new IOException("This is not a SSH_MSG_USERAUTH_BANNER! (" + packet_type + ")"); + + message = tr.readString("UTF-8"); + language = tr.readString(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_REQUEST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_BANNER); + tw.writeString(message); + tw.writeString(language); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java new file mode 100644 index 0000000..fd4a726 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java @@ -0,0 +1,53 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthBanner. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthFailure.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthFailure +{ + byte[] payload; + + String[] authThatCanContinue; + boolean partialSuccess; + + public PacketUserauthFailure(String[] authThatCanContinue, boolean partialSuccess) + { + this.authThatCanContinue = authThatCanContinue; + this.partialSuccess = partialSuccess; + } + + public PacketUserauthFailure(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_FAILURE) + throw new IOException("This is not a SSH_MSG_USERAUTH_FAILURE! (" + packet_type + ")"); + + authThatCanContinue = tr.readNameList(); + partialSuccess = tr.readBoolean(); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_FAILURE packet!"); + } + + public String[] getAuthThatCanContinue() + { + return authThatCanContinue; + } + + public boolean isPartialSuccess() + { + return partialSuccess; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java new file mode 100644 index 0000000..e1606d1 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java @@ -0,0 +1,84 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthInfoRequest. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthInfoRequest.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthInfoRequest +{ + byte[] payload; + + String name; + String instruction; + String languageTag; + int numPrompts; + + String prompt[]; + boolean echo[]; + + public PacketUserauthInfoRequest(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_INFO_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_INFO_REQUEST! (" + packet_type + ")"); + + name = tr.readString(); + instruction = tr.readString(); + languageTag = tr.readString(); + + numPrompts = tr.readUINT32(); + + prompt = new String[numPrompts]; + echo = new boolean[numPrompts]; + + for (int i = 0; i < numPrompts; i++) + { + prompt[i] = tr.readString(); + echo[i] = tr.readBoolean(); + } + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_INFO_REQUEST packet!"); + } + + public boolean[] getEcho() + { + return echo; + } + + public String getInstruction() + { + return instruction; + } + + public String getLanguageTag() + { + return languageTag; + } + + public String getName() + { + return name; + } + + public int getNumPrompts() + { + return numPrompts; + } + + public String[] getPrompt() + { + return prompt; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java new file mode 100644 index 0000000..e8795d4 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java @@ -0,0 +1,35 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketUserauthInfoResponse. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthInfoResponse.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthInfoResponse +{ + byte[] payload; + + String[] responses; + + public PacketUserauthInfoResponse(String[] responses) + { + this.responses = responses; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_INFO_RESPONSE); + tw.writeUINT32(responses.length); + for (int i = 0; i < responses.length; i++) + tw.writeString(responses[i]); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java new file mode 100644 index 0000000..83e9f49 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java @@ -0,0 +1,42 @@ + +package com.trilead.ssh2.packets; + +/** + * PacketUserauthRequestInteractive. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestInteractive.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestInteractive +{ + byte[] payload; + + String userName; + String serviceName; + String[] submethods; + + public PacketUserauthRequestInteractive(String serviceName, String user, String[] submethods) + { + this.serviceName = serviceName; + this.userName = user; + this.submethods = submethods; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName); + tw.writeString(serviceName); + tw.writeString("keyboard-interactive"); + tw.writeString(""); // draft-ietf-secsh-newmodes-04.txt says that + // the language tag should be empty. + tw.writeNameList(submethods); + + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java new file mode 100644 index 0000000..d786003 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java @@ -0,0 +1,61 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthRequestPassword. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestNone.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestNone +{ + byte[] payload; + + String userName; + String serviceName; + + public PacketUserauthRequestNone(String serviceName, String user) + { + this.serviceName = serviceName; + this.userName = user; + } + + public PacketUserauthRequestNone(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST! (" + packet_type + ")"); + + userName = tr.readString(); + serviceName = tr.readString(); + + String method = tr.readString(); + + if (method.equals("none") == false) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST with type none!"); + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_REQUEST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName); + tw.writeString(serviceName); + tw.writeString("none"); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java new file mode 100644 index 0000000..83047dd --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java @@ -0,0 +1,67 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthRequestPassword. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestPassword.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestPassword +{ + byte[] payload; + + String userName; + String serviceName; + String password; + + public PacketUserauthRequestPassword(String serviceName, String user, String pass) + { + this.serviceName = serviceName; + this.userName = user; + this.password = pass; + } + + public PacketUserauthRequestPassword(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST! (" + packet_type + ")"); + + userName = tr.readString(); + serviceName = tr.readString(); + + String method = tr.readString(); + + if (method.equals("password") == false) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST with type password!"); + + /* ... */ + + if (tr.remain() != 0) + throw new IOException("Padding in SSH_MSG_USERAUTH_REQUEST packet!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName); + tw.writeString(serviceName); + tw.writeString("password"); + tw.writeBoolean(false); + tw.writeString(password); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java new file mode 100644 index 0000000..6462864 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java @@ -0,0 +1,65 @@ +package com.trilead.ssh2.packets; + +import java.io.IOException; + +/** + * PacketUserauthRequestPublicKey. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: PacketUserauthRequestPublicKey.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class PacketUserauthRequestPublicKey +{ + byte[] payload; + + String userName; + String serviceName; + String password; + String pkAlgoName; + byte[] pk; + byte[] sig; + + public PacketUserauthRequestPublicKey(String serviceName, String user, + String pkAlgorithmName, byte[] pk, byte[] sig) + { + this.serviceName = serviceName; + this.userName = user; + this.pkAlgoName = pkAlgorithmName; + this.pk = pk; + this.sig = sig; + } + + public PacketUserauthRequestPublicKey(byte payload[], int off, int len) throws IOException + { + this.payload = new byte[len]; + System.arraycopy(payload, off, this.payload, 0, len); + + TypesReader tr = new TypesReader(payload, off, len); + + int packet_type = tr.readByte(); + + if (packet_type != Packets.SSH_MSG_USERAUTH_REQUEST) + throw new IOException("This is not a SSH_MSG_USERAUTH_REQUEST! (" + + packet_type + ")"); + + throw new IOException("Not implemented!"); + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(userName); + tw.writeString(serviceName); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString(pkAlgoName); + tw.writeString(pk, 0, pk.length); + tw.writeString(sig, 0, sig.length); + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/Packets.java b/app/src/main/java/com/trilead/ssh2/packets/Packets.java new file mode 100644 index 0000000..6989286 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/Packets.java @@ -0,0 +1,149 @@ + +package com.trilead.ssh2.packets; + +/** + * Packets. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Packets.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class Packets +{ + public static final int SSH_MSG_DISCONNECT = 1; + public static final int SSH_MSG_IGNORE = 2; + public static final int SSH_MSG_UNIMPLEMENTED = 3; + public static final int SSH_MSG_DEBUG = 4; + public static final int SSH_MSG_SERVICE_REQUEST = 5; + public static final int SSH_MSG_SERVICE_ACCEPT = 6; + + public static final int SSH_MSG_KEXINIT = 20; + public static final int SSH_MSG_NEWKEYS = 21; + + public static final int SSH_MSG_KEXDH_INIT = 30; + public static final int SSH_MSG_KEXDH_REPLY = 31; + + public static final int SSH_MSG_KEX_DH_GEX_REQUEST_OLD = 30; + public static final int SSH_MSG_KEX_DH_GEX_REQUEST = 34; + public static final int SSH_MSG_KEX_DH_GEX_GROUP = 31; + public static final int SSH_MSG_KEX_DH_GEX_INIT = 32; + public static final int SSH_MSG_KEX_DH_GEX_REPLY = 33; + + public static final int SSH_MSG_USERAUTH_REQUEST = 50; + public static final int SSH_MSG_USERAUTH_FAILURE = 51; + public static final int SSH_MSG_USERAUTH_SUCCESS = 52; + public static final int SSH_MSG_USERAUTH_BANNER = 53; + public static final int SSH_MSG_USERAUTH_INFO_REQUEST = 60; + public static final int SSH_MSG_USERAUTH_INFO_RESPONSE = 61; + + public static final int SSH_MSG_GLOBAL_REQUEST = 80; + public static final int SSH_MSG_REQUEST_SUCCESS = 81; + public static final int SSH_MSG_REQUEST_FAILURE = 82; + + public static final int SSH_MSG_CHANNEL_OPEN = 90; + public static final int SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91; + public static final int SSH_MSG_CHANNEL_OPEN_FAILURE = 92; + public static final int SSH_MSG_CHANNEL_WINDOW_ADJUST = 93; + public static final int SSH_MSG_CHANNEL_DATA = 94; + public static final int SSH_MSG_CHANNEL_EXTENDED_DATA = 95; + public static final int SSH_MSG_CHANNEL_EOF = 96; + public static final int SSH_MSG_CHANNEL_CLOSE = 97; + public static final int SSH_MSG_CHANNEL_REQUEST = 98; + public static final int SSH_MSG_CHANNEL_SUCCESS = 99; + public static final int SSH_MSG_CHANNEL_FAILURE = 100; + + public static final int SSH_EXTENDED_DATA_STDERR = 1; + + public static final int SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1; + public static final int SSH_DISCONNECT_PROTOCOL_ERROR = 2; + public static final int SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3; + public static final int SSH_DISCONNECT_RESERVED = 4; + public static final int SSH_DISCONNECT_MAC_ERROR = 5; + public static final int SSH_DISCONNECT_COMPRESSION_ERROR = 6; + public static final int SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7; + public static final int SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8; + public static final int SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9; + public static final int SSH_DISCONNECT_CONNECTION_LOST = 10; + public static final int SSH_DISCONNECT_BY_APPLICATION = 11; + public static final int SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12; + public static final int SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13; + public static final int SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14; + public static final int SSH_DISCONNECT_ILLEGAL_USER_NAME = 15; + + public static final int SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1; + public static final int SSH_OPEN_CONNECT_FAILED = 2; + public static final int SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3; + public static final int SSH_OPEN_RESOURCE_SHORTAGE = 4; + + private static final String[] reverseNames = new String[101]; + + static + { + reverseNames[1] = "SSH_MSG_DISCONNECT"; + reverseNames[2] = "SSH_MSG_IGNORE"; + reverseNames[3] = "SSH_MSG_UNIMPLEMENTED"; + reverseNames[4] = "SSH_MSG_DEBUG"; + reverseNames[5] = "SSH_MSG_SERVICE_REQUEST"; + reverseNames[6] = "SSH_MSG_SERVICE_ACCEPT"; + + reverseNames[20] = "SSH_MSG_KEXINIT"; + reverseNames[21] = "SSH_MSG_NEWKEYS"; + + reverseNames[30] = "SSH_MSG_KEXDH_INIT"; + reverseNames[31] = "SSH_MSG_KEXDH_REPLY/SSH_MSG_KEX_DH_GEX_GROUP"; + reverseNames[32] = "SSH_MSG_KEX_DH_GEX_INIT"; + reverseNames[33] = "SSH_MSG_KEX_DH_GEX_REPLY"; + reverseNames[34] = "SSH_MSG_KEX_DH_GEX_REQUEST"; + + reverseNames[50] = "SSH_MSG_USERAUTH_REQUEST"; + reverseNames[51] = "SSH_MSG_USERAUTH_FAILURE"; + reverseNames[52] = "SSH_MSG_USERAUTH_SUCCESS"; + reverseNames[53] = "SSH_MSG_USERAUTH_BANNER"; + + reverseNames[60] = "SSH_MSG_USERAUTH_INFO_REQUEST"; + reverseNames[61] = "SSH_MSG_USERAUTH_INFO_RESPONSE"; + + reverseNames[80] = "SSH_MSG_GLOBAL_REQUEST"; + reverseNames[81] = "SSH_MSG_REQUEST_SUCCESS"; + reverseNames[82] = "SSH_MSG_REQUEST_FAILURE"; + + reverseNames[90] = "SSH_MSG_CHANNEL_OPEN"; + reverseNames[91] = "SSH_MSG_CHANNEL_OPEN_CONFIRMATION"; + reverseNames[92] = "SSH_MSG_CHANNEL_OPEN_FAILURE"; + reverseNames[93] = "SSH_MSG_CHANNEL_WINDOW_ADJUST"; + reverseNames[94] = "SSH_MSG_CHANNEL_DATA"; + reverseNames[95] = "SSH_MSG_CHANNEL_EXTENDED_DATA"; + reverseNames[96] = "SSH_MSG_CHANNEL_EOF"; + reverseNames[97] = "SSH_MSG_CHANNEL_CLOSE"; + reverseNames[98] = "SSH_MSG_CHANNEL_REQUEST"; + reverseNames[99] = "SSH_MSG_CHANNEL_SUCCESS"; + reverseNames[100] = "SSH_MSG_CHANNEL_FAILURE"; + } + + public static final String getMessageName(int type) + { + String res = null; + + if ((type >= 0) && (type < reverseNames.length)) + { + res = reverseNames[type]; + } + + return (res == null) ? ("UNKNOWN MSG " + type) : res; + } + + // public static final void debug(String tag, byte[] msg) + // { + // System.err.println(tag + " Type: " + msg[0] + ", LEN: " + msg.length); + // + // for (int i = 0; i < msg.length; i++) + // { + // if (((msg[i] >= 'a') && (msg[i] <= 'z')) || ((msg[i] >= 'A') && (msg[i] <= 'Z')) + // || ((msg[i] >= '0') && (msg[i] <= '9')) || (msg[i] == ' ')) + // System.err.print((char) msg[i]); + // else + // System.err.print("."); + // } + // System.err.println(); + // System.err.flush(); + // } +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/TypesReader.java b/app/src/main/java/com/trilead/ssh2/packets/TypesReader.java new file mode 100644 index 0000000..28f5363 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/TypesReader.java @@ -0,0 +1,177 @@ + +package com.trilead.ssh2.packets; + +import java.io.IOException; +import java.math.BigInteger; + +import com.trilead.ssh2.util.Tokenizer; + + +/** + * TypesReader. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TypesReader.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TypesReader +{ + byte[] arr; + int pos = 0; + int max = 0; + + public TypesReader(byte[] arr) + { + this.arr = arr; + pos = 0; + max = arr.length; + } + + public TypesReader(byte[] arr, int off) + { + this.arr = arr; + this.pos = off; + this.max = arr.length; + + if ((pos < 0) || (pos > arr.length)) + throw new IllegalArgumentException("Illegal offset."); + } + + public TypesReader(byte[] arr, int off, int len) + { + this.arr = arr; + this.pos = off; + this.max = off + len; + + if ((pos < 0) || (pos > arr.length)) + throw new IllegalArgumentException("Illegal offset."); + + if ((max < 0) || (max > arr.length)) + throw new IllegalArgumentException("Illegal length."); + } + + public int readByte() throws IOException + { + if (pos >= max) + throw new IOException("Packet too short."); + + return (arr[pos++] & 0xff); + } + + public byte[] readBytes(int len) throws IOException + { + if ((pos + len) > max) + throw new IOException("Packet too short."); + + byte[] res = new byte[len]; + + System.arraycopy(arr, pos, res, 0, len); + pos += len; + + return res; + } + + public void readBytes(byte[] dst, int off, int len) throws IOException + { + if ((pos + len) > max) + throw new IOException("Packet too short."); + + System.arraycopy(arr, pos, dst, off, len); + pos += len; + } + + public boolean readBoolean() throws IOException + { + if (pos >= max) + throw new IOException("Packet too short."); + + return (arr[pos++] != 0); + } + + public int readUINT32() throws IOException + { + if ((pos + 4) > max) + throw new IOException("Packet too short."); + + return ((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) + | (arr[pos++] & 0xff); + } + + public long readUINT64() throws IOException + { + if ((pos + 8) > max) + throw new IOException("Packet too short."); + + long high = ((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) + | (arr[pos++] & 0xff); /* sign extension may take place - will be shifted away =) */ + + long low = ((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) + | (arr[pos++] & 0xff); /* sign extension may take place - handle below */ + + return (high << 32) | (low & 0xffffffffl); /* see Java language spec (15.22.1, 5.6.2) */ + } + + public BigInteger readMPINT() throws IOException + { + BigInteger b; + + byte raw[] = readByteString(); + + if (raw.length == 0) + b = BigInteger.ZERO; + else + b = new BigInteger(raw); + + return b; + } + + public byte[] readByteString() throws IOException + { + int len = readUINT32(); + + if ((len + pos) > max) + throw new IOException("Malformed SSH byte string."); + + byte[] res = new byte[len]; + System.arraycopy(arr, pos, res, 0, len); + pos += len; + return res; + } + + public String readString(String charsetName) throws IOException + { + int len = readUINT32(); + + if ((len + pos) > max) + throw new IOException("Malformed SSH string."); + + String res = (charsetName == null) ? new String(arr, pos, len) : new String(arr, pos, len, charsetName); + pos += len; + + return res; + } + + public String readString() throws IOException + { + int len = readUINT32(); + + if ((len + pos) > max) + throw new IOException("Malformed SSH string."); + + String res = new String(arr, pos, len, "ISO-8859-1"); + + pos += len; + + return res; + } + + public String[] readNameList() throws IOException + { + return Tokenizer.parseTokens(readString(), ','); + } + + public int remain() + { + return max - pos; + } + +} diff --git a/app/src/main/java/com/trilead/ssh2/packets/TypesWriter.java b/app/src/main/java/com/trilead/ssh2/packets/TypesWriter.java new file mode 100644 index 0000000..9f7336b --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/packets/TypesWriter.java @@ -0,0 +1,169 @@ + +package com.trilead.ssh2.packets; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +/** + * TypesWriter. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TypesWriter.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TypesWriter +{ + byte arr[]; + int pos; + + public TypesWriter() + { + arr = new byte[256]; + pos = 0; + } + + private void resize(int len) + { + byte new_arr[] = new byte[len]; + System.arraycopy(arr, 0, new_arr, 0, arr.length); + arr = new_arr; + } + + public int length() + { + return pos; + } + + public byte[] getBytes() + { + byte[] dst = new byte[pos]; + System.arraycopy(arr, 0, dst, 0, pos); + return dst; + } + + public void getBytes(byte dst[]) + { + System.arraycopy(arr, 0, dst, 0, pos); + } + + public void writeUINT32(int val, int off) + { + if ((off + 4) > arr.length) + resize(off + 32); + + arr[off++] = (byte) (val >> 24); + arr[off++] = (byte) (val >> 16); + arr[off++] = (byte) (val >> 8); + arr[off++] = (byte) val; + } + + public void writeUINT32(int val) + { + writeUINT32(val, pos); + pos += 4; + } + + public void writeUINT64(long val) + { + if ((pos + 8) > arr.length) + resize(arr.length + 32); + + arr[pos++] = (byte) (val >> 56); + arr[pos++] = (byte) (val >> 48); + arr[pos++] = (byte) (val >> 40); + arr[pos++] = (byte) (val >> 32); + arr[pos++] = (byte) (val >> 24); + arr[pos++] = (byte) (val >> 16); + arr[pos++] = (byte) (val >> 8); + arr[pos++] = (byte) val; + } + + public void writeBoolean(boolean v) + { + if ((pos + 1) > arr.length) + resize(arr.length + 32); + + arr[pos++] = v ? (byte) 1 : (byte) 0; + } + + public void writeByte(int v, int off) + { + if ((off + 1) > arr.length) + resize(off + 32); + + arr[off] = (byte) v; + } + + public void writeByte(int v) + { + writeByte(v, pos); + pos++; + } + + public void writeMPInt(BigInteger b) + { + byte raw[] = b.toByteArray(); + + if ((raw.length == 1) && (raw[0] == 0)) + writeUINT32(0); /* String with zero bytes of data */ + else + writeString(raw, 0, raw.length); + } + + public void writeBytes(byte[] buff) + { + writeBytes(buff, 0, buff.length); + } + + public void writeBytes(byte[] buff, int off, int len) + { + if ((pos + len) > arr.length) + resize(arr.length + len + 32); + + System.arraycopy(buff, off, arr, pos, len); + pos += len; + } + + public void writeString(byte[] buff, int off, int len) + { + writeUINT32(len); + writeBytes(buff, off, len); + } + + public void writeString(String v) + { + byte[] b; + + try + { + /* All Java JVMs must support ISO-8859-1 */ + b = v.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException ignore) + { + b = v.getBytes(); + } + + writeUINT32(b.length); + writeBytes(b, 0, b.length); + } + + public void writeString(String v, String charsetName) throws UnsupportedEncodingException + { + byte[] b = (charsetName == null) ? v.getBytes() : v.getBytes(charsetName); + + writeUINT32(b.length); + writeBytes(b, 0, b.length); + } + + public void writeNameList(String v[]) + { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < v.length; i++) + { + if (i > 0) + sb.append(','); + sb.append(v[i]); + } + writeString(sb.toString()); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java b/app/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java new file mode 100644 index 0000000..19f0525 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java @@ -0,0 +1,38 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Values for the 'text-hint' field in the SFTP ATTRS data type. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttrTextHints.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttrTextHints +{ + /** + * The server knows the file is a text file, and should be opened + * using the SSH_FXF_ACCESS_TEXT_MODE flag. + */ + public static final int SSH_FILEXFER_ATTR_KNOWN_TEXT = 0x00; + + /** + * The server has applied a heuristic or other mechanism and + * believes that the file should be opened with the + * SSH_FXF_ACCESS_TEXT_MODE flag. + */ + public static final int SSH_FILEXFER_ATTR_GUESSED_TEXT = 0x01; + + /** + * The server knows the file has binary content. + */ + public static final int SSH_FILEXFER_ATTR_KNOWN_BINARY = 0x02; + + /** + * The server has applied a heuristic or other mechanism and + * believes has binary content, and should not be opened with the + * SSH_FXF_ACCESS_TEXT_MODE flag. + */ + public static final int SSH_FILEXFER_ATTR_GUESSED_BINARY = 0x03; +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/AttribBits.java b/app/src/main/java/com/trilead/ssh2/sftp/AttribBits.java new file mode 100644 index 0000000..b143613 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/AttribBits.java @@ -0,0 +1,129 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Attribute Bits for the "attrib-bits" and "attrib-bits-valid" fields + * of the SFTP ATTR data type. + *

+ * Yes, these are the "attrib-bits", even though they have "_FLAGS_" in + * their name. Don't ask - I did not invent it. + *

+ * "These fields, taken together, reflect various attributes of the file + * or directory, on the server. Bits not set in 'attrib-bits-valid' MUST be + * ignored in the 'attrib-bits' field. This allows both the server and the + * client to communicate only the bits it knows about without inadvertently + * twiddling bits they don't understand." + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribBits.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribBits +{ + /** + * Advisory, read-only bit. This bit is not part of the access + * control information on the file, but is rather an advisory field + * indicating that the file should not be written. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_READONLY = 0x00000001; + + /** + * The file is part of the operating system. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_SYSTEM = 0x00000002; + + /** + * File SHOULD NOT be shown to user unless specifically requested. + * For example, most UNIX systems SHOULD set this bit if the filename + * begins with a 'period'. This bit may be read-only (see section 5.4 of + * the SFTP standard draft). Most UNIX systems will not allow this to be + * changed. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_HIDDEN = 0x00000004; + + /** + * This attribute applies only to directories. This attribute is + * always read-only, and cannot be modified. This attribute means + * that files and directory names in this directory should be compared + * without regard to case. + *

+ * It is recommended that where possible, the server's filesystem be + * allowed to do comparisons. For example, if a client wished to prompt + * a user before overwriting a file, it should not compare the new name + * with the previously retrieved list of names in the directory. Rather, + * it should first try to create the new file by specifying + * SSH_FXF_CREATE_NEW flag. Then, if this fails and returns + * SSH_FX_FILE_ALREADY_EXISTS, it should prompt the user and then retry + * the create specifying SSH_FXF_CREATE_TRUNCATE. + *

+ * Unless otherwise specified, filenames are assumed to be case sensitive. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE = 0x00000008; + + /** + * The file should be included in backup / archive operations. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_ARCHIVE = 0x00000010; + + /** + * The file is stored on disk using file-system level transparent + * encryption. This flag does not affect the file data on the wire + * (for either READ or WRITE requests.) + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED = 0x00000020; + + /** + * The file is stored on disk using file-system level transparent + * compression. This flag does not affect the file data on the wire. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_COMPRESSED = 0x00000040; + + /** + * The file is a sparse file; this means that file blocks that have + * not been explicitly written are not stored on disk. For example, if + * a client writes a buffer at 10 M from the beginning of the file, + * the blocks between the previous EOF marker and the 10 M offset would + * not consume physical disk space. + *

+ * Some servers may store all files as sparse files, in which case + * this bit will be unconditionally set. Other servers may not have + * a mechanism for determining if the file is sparse, and so the file + * MAY be stored sparse even if this flag is not set. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_SPARSE = 0x00000080; + + /** + * Opening the file without either the SSH_FXF_ACCESS_APPEND_DATA or + * the SSH_FXF_ACCESS_APPEND_DATA_ATOMIC flag (see section 8.1.1.3 + * of the SFTP standard draft) MUST result in an + * SSH_FX_INVALID_PARAMETER error. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY = 0x00000100; + + /** + * The file cannot be deleted or renamed, no hard link can be created + * to this file, and no data can be written to the file. + *

+ * This bit implies a stronger level of protection than + * SSH_FILEXFER_ATTR_FLAGS_READONLY, the file permission mask or ACLs. + * Typically even the superuser cannot write to immutable files, and + * only the superuser can set or remove the bit. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE = 0x00000200; + + /** + * When the file is modified, the changes are written synchronously + * to the disk. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_SYNC = 0x00000400; + + /** + * The server MAY include this bit in a directory listing or realpath + * response. It indicates there was a failure in the translation to UTF-8. + * If this flag is included, the server SHOULD also include the + * UNTRANSLATED_NAME attribute. + */ + public static final int SSH_FILEXFER_ATTR_FLAGS_TRANSLATION_ERR = 0x00000800; + +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java b/app/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java new file mode 100644 index 0000000..ea27871 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java @@ -0,0 +1,112 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Attribute Flags. The 'valid-attribute-flags' field in + * the SFTP ATTRS data type specifies which of the fields are actually present. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribFlags.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribFlags +{ + /** + * Indicates that the 'allocation-size' field is present. + */ + public static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001; + + /** Protocol version 6: + * 0x00000002 was used in a previous version of this protocol. + * It is now a reserved value and MUST NOT appear in the mask. + * Some future version of this protocol may reuse this value. + */ + public static final int SSH_FILEXFER_ATTR_V3_UIDGID = 0x00000002; + + /** + * Indicates that the 'permissions' field is present. + */ + public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004; + + /** + * Indicates that the 'atime' and 'mtime' field are present + * (protocol v3). + */ + public static final int SSH_FILEXFER_ATTR_V3_ACMODTIME = 0x00000008; + + /** + * Indicates that the 'atime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_ACCESSTIME = 0x00000008; + + /** + * Indicates that the 'createtime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_CREATETIME = 0x00000010; + + /** + * Indicates that the 'mtime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_MODIFYTIME = 0x00000020; + + /** + * Indicates that the 'acl' field is present. + */ + public static final int SSH_FILEXFER_ATTR_ACL = 0x00000040; + + /** + * Indicates that the 'owner' and 'group' fields are present. + */ + public static final int SSH_FILEXFER_ATTR_OWNERGROUP = 0x00000080; + + /** + * Indicates that additionally to the 'atime', 'createtime', + * 'mtime' and 'ctime' fields (if present), there is also + * 'atime-nseconds', 'createtime-nseconds', 'mtime-nseconds' + * and 'ctime-nseconds'. + */ + public static final int SSH_FILEXFER_ATTR_SUBSECOND_TIMES = 0x00000100; + + /** + * Indicates that the 'attrib-bits' and 'attrib-bits-valid' + * fields are present. + */ + public static final int SSH_FILEXFER_ATTR_BITS = 0x00000200; + + /** + * Indicates that the 'allocation-size' field is present. + */ + public static final int SSH_FILEXFER_ATTR_ALLOCATION_SIZE = 0x00000400; + + /** + * Indicates that the 'text-hint' field is present. + */ + public static final int SSH_FILEXFER_ATTR_TEXT_HINT = 0x00000800; + + /** + * Indicates that the 'mime-type' field is present. + */ + public static final int SSH_FILEXFER_ATTR_MIME_TYPE = 0x00001000; + + /** + * Indicates that the 'link-count' field is present. + */ + public static final int SSH_FILEXFER_ATTR_LINK_COUNT = 0x00002000; + + /** + * Indicates that the 'untranslated-name' field is present. + */ + public static final int SSH_FILEXFER_ATTR_UNTRANSLATED_NAME = 0x00004000; + + /** + * Indicates that the 'ctime' field is present. + */ + public static final int SSH_FILEXFER_ATTR_CTIME = 0x00008000; + + /** + * Indicates that the 'extended-count' field (and probablby some + * 'extensions') is present. + */ + public static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000; +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java b/app/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java new file mode 100644 index 0000000..558aa6f --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java @@ -0,0 +1,32 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Permissions for the 'permissions' field in the SFTP ATTRS data type. + *

+ * "The 'permissions' field contains a bit mask specifying file permissions. + * These permissions correspond to the st_mode field of the stat structure + * defined by POSIX [IEEE.1003-1.1996]." + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribPermissions.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribPermissions +{ + /* Octal values! */ + + public static final int S_IRUSR = 0400; + public static final int S_IWUSR = 0200; + public static final int S_IXUSR = 0100; + public static final int S_IRGRP = 0040; + public static final int S_IWGRP = 0020; + public static final int S_IXGRP = 0010; + public static final int S_IROTH = 0004; + public static final int S_IWOTH = 0002; + public static final int S_IXOTH = 0001; + public static final int S_ISUID = 04000; + public static final int S_ISGID = 02000; + public static final int S_ISVTX = 01000; +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java b/app/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java new file mode 100644 index 0000000..e2f4169 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java @@ -0,0 +1,28 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * Types for the 'type' field in the SFTP ATTRS data type. + *

+ * "On a POSIX system, these values would be derived from the mode field + * of the stat structure. SPECIAL should be used for files that are of + * a known type which cannot be expressed in the protocol. UNKNOWN + * should be used if the type is not known." + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: AttribTypes.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class AttribTypes +{ + public static final int SSH_FILEXFER_TYPE_REGULAR = 1; + public static final int SSH_FILEXFER_TYPE_DIRECTORY = 2; + public static final int SSH_FILEXFER_TYPE_SYMLINK = 3; + public static final int SSH_FILEXFER_TYPE_SPECIAL = 4; + public static final int SSH_FILEXFER_TYPE_UNKNOWN = 5; + public static final int SSH_FILEXFER_TYPE_SOCKET = 6; + public static final int SSH_FILEXFER_TYPE_CHAR_DEVICE = 7; + public static final int SSH_FILEXFER_TYPE_BLOCK_DEVICE = 8; + public static final int SSH_FILEXFER_TYPE_FIFO = 9; +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java b/app/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java new file mode 100644 index 0000000..7317a00 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java @@ -0,0 +1,104 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Error Codes + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ErrorCodes.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class ErrorCodes +{ + public static final int SSH_FX_OK = 0; + public static final int SSH_FX_EOF = 1; + public static final int SSH_FX_NO_SUCH_FILE = 2; + public static final int SSH_FX_PERMISSION_DENIED = 3; + public static final int SSH_FX_FAILURE = 4; + public static final int SSH_FX_BAD_MESSAGE = 5; + public static final int SSH_FX_NO_CONNECTION = 6; + public static final int SSH_FX_CONNECTION_LOST = 7; + public static final int SSH_FX_OP_UNSUPPORTED = 8; + public static final int SSH_FX_INVALID_HANDLE = 9; + public static final int SSH_FX_NO_SUCH_PATH = 10; + public static final int SSH_FX_FILE_ALREADY_EXISTS = 11; + public static final int SSH_FX_WRITE_PROTECT = 12; + public static final int SSH_FX_NO_MEDIA = 13; + public static final int SSH_FX_NO_SPACE_ON_FILESYSTEM = 14; + public static final int SSH_FX_QUOTA_EXCEEDED = 15; + public static final int SSH_FX_UNKNOWN_PRINCIPAL = 16; + public static final int SSH_FX_LOCK_CONFLICT = 17; + public static final int SSH_FX_DIR_NOT_EMPTY = 18; + public static final int SSH_FX_NOT_A_DIRECTORY = 19; + public static final int SSH_FX_INVALID_FILENAME = 20; + public static final int SSH_FX_LINK_LOOP = 21; + public static final int SSH_FX_CANNOT_DELETE = 22; + public static final int SSH_FX_INVALID_PARAMETER = 23; + public static final int SSH_FX_FILE_IS_A_DIRECTORY = 24; + public static final int SSH_FX_BYTE_RANGE_LOCK_CONFLICT = 25; + public static final int SSH_FX_BYTE_RANGE_LOCK_REFUSED = 26; + public static final int SSH_FX_DELETE_PENDING = 27; + public static final int SSH_FX_FILE_CORRUPT = 28; + public static final int SSH_FX_OWNER_INVALID = 29; + public static final int SSH_FX_GROUP_INVALID = 30; + public static final int SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK = 31; + + private static final String[][] messages = { + + { "SSH_FX_OK", "Indicates successful completion of the operation." }, + { "SSH_FX_EOF", + "An attempt to read past the end-of-file was made; or, there are no more directory entries to return." }, + { "SSH_FX_NO_SUCH_FILE", "A reference was made to a file which does not exist." }, + { "SSH_FX_PERMISSION_DENIED", "The user does not have sufficient permissions to perform the operation." }, + { "SSH_FX_FAILURE", "An error occurred, but no specific error code exists to describe the failure." }, + { "SSH_FX_BAD_MESSAGE", "A badly formatted packet or other SFTP protocol incompatibility was detected." }, + { "SSH_FX_NO_CONNECTION", "There is no connection to the server." }, + { "SSH_FX_CONNECTION_LOST", "The connection to the server was lost." }, + { "SSH_FX_OP_UNSUPPORTED", + "An attempted operation could not be completed by the server because the server does not support the operation." }, + { "SSH_FX_INVALID_HANDLE", "The handle value was invalid." }, + { "SSH_FX_NO_SUCH_PATH", "The file path does not exist or is invalid." }, + { "SSH_FX_FILE_ALREADY_EXISTS", "The file already exists." }, + { "SSH_FX_WRITE_PROTECT", "The file is on read-only media, or the media is write protected." }, + { "SSH_FX_NO_MEDIA", + "The requested operation cannot be completed because there is no media available in the drive." }, + { "SSH_FX_NO_SPACE_ON_FILESYSTEM", + "The requested operation cannot be completed because there is insufficient free space on the filesystem." }, + { "SSH_FX_QUOTA_EXCEEDED", + "The operation cannot be completed because it would exceed the user's storage quota." }, + { + "SSH_FX_UNKNOWN_PRINCIPAL", + "A principal referenced by the request (either the 'owner', 'group', or 'who' field of an ACL), was unknown. The error specific data contains the problematic names." }, + { "SSH_FX_LOCK_CONFLICT", "The file could not be opened because it is locked by another process." }, + { "SSH_FX_DIR_NOT_EMPTY", "The directory is not empty." }, + { "SSH_FX_NOT_A_DIRECTORY", "The specified file is not a directory." }, + { "SSH_FX_INVALID_FILENAME", "The filename is not valid." }, + { "SSH_FX_LINK_LOOP", + "Too many symbolic links encountered or, an SSH_FXF_NOFOLLOW open encountered a symbolic link as the final component." }, + { "SSH_FX_CANNOT_DELETE", + "The file cannot be deleted. One possible reason is that the advisory READONLY attribute-bit is set." }, + { "SSH_FX_INVALID_PARAMETER", + "One of the parameters was out of range, or the parameters specified cannot be used together." }, + { "SSH_FX_FILE_IS_A_DIRECTORY", + "The specified file was a directory in a context where a directory cannot be used." }, + { "SSH_FX_BYTE_RANGE_LOCK_CONFLICT", + " A read or write operation failed because another process's mandatory byte-range lock overlaps with the request." }, + { "SSH_FX_BYTE_RANGE_LOCK_REFUSED", "A request for a byte range lock was refused." }, + { "SSH_FX_DELETE_PENDING", "An operation was attempted on a file for which a delete operation is pending." }, + { "SSH_FX_FILE_CORRUPT", "The file is corrupt; an filesystem integrity check should be run." }, + { "SSH_FX_OWNER_INVALID", "The principal specified can not be assigned as an owner of a file." }, + { "SSH_FX_GROUP_INVALID", "The principal specified can not be assigned as the primary group of a file." }, + { "SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK", + "The requested operation could not be completed because the specifed byte range lock has not been granted." }, + + }; + + public static final String[] getDescription(int errorCode) + { + if ((errorCode < 0) || (errorCode >= messages.length)) + return null; + + return messages[errorCode]; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java b/app/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java new file mode 100644 index 0000000..b2979b9 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java @@ -0,0 +1,223 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Open Flags. + * + * The following table is provided to assist in mapping POSIX semantics + * to equivalent SFTP file open parameters: + *

+ * TODO: This comment should be moved to the open method. + *

+ *

    + *
  • O_RDONLY + *
    • desired-access = READ_DATA | READ_ATTRIBUTES
    + *
  • + *
+ *
    + *
  • O_WRONLY + *
    • desired-access = WRITE_DATA | WRITE_ATTRIBUTES
    + *
  • + *
+ *
    + *
  • O_RDWR + *
    • desired-access = READ_DATA | READ_ATTRIBUTES | WRITE_DATA | WRITE_ATTRIBUTES
    + *
  • + *
+ *
    + *
  • O_APPEND + *
      + *
    • desired-access = WRITE_DATA | WRITE_ATTRIBUTES | APPEND_DATA
    • + *
    • flags = SSH_FXF_ACCESS_APPEND_DATA and or SSH_FXF_ACCESS_APPEND_DATA_ATOMIC
    • + *
    + *
  • + *
+ *
    + *
  • O_CREAT + *
      + *
    • flags = SSH_FXF_OPEN_OR_CREATE
    • + *
    + *
  • + *
+ *
    + *
  • O_TRUNC + *
      + *
    • flags = SSH_FXF_TRUNCATE_EXISTING
    • + *
    + *
  • + *
+ *
    + *
  • O_TRUNC|O_CREATE + *
      + *
    • flags = SSH_FXF_CREATE_TRUNCATE
    • + *
    + *
  • + *
+ * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: OpenFlags.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + */ +public class OpenFlags +{ + /** + * Disposition is a 3 bit field that controls how the file is opened. + * The server MUST support these bits (possible enumaration values: + * SSH_FXF_CREATE_NEW, SSH_FXF_CREATE_TRUNCATE, SSH_FXF_OPEN_EXISTING, + * SSH_FXF_OPEN_OR_CREATE or SSH_FXF_TRUNCATE_EXISTING). + */ + public static final int SSH_FXF_ACCESS_DISPOSITION = 0x00000007; + + /** + * A new file is created; if the file already exists, the server + * MUST return status SSH_FX_FILE_ALREADY_EXISTS. + */ + public static final int SSH_FXF_CREATE_NEW = 0x00000000; + + /** + * A new file is created; if the file already exists, it is opened + * and truncated. + */ + public static final int SSH_FXF_CREATE_TRUNCATE = 0x00000001; + + /** + * An existing file is opened. If the file does not exist, the + * server MUST return SSH_FX_NO_SUCH_FILE. If a directory in the + * path does not exist, the server SHOULD return + * SSH_FX_NO_SUCH_PATH. It is also acceptable if the server + * returns SSH_FX_NO_SUCH_FILE in this case. + */ + public static final int SSH_FXF_OPEN_EXISTING = 0x00000002; + + /** + * If the file exists, it is opened. If the file does not exist, + * it is created. + */ + public static final int SSH_FXF_OPEN_OR_CREATE = 0x00000003; + + /** + * An existing file is opened and truncated. If the file does not + * exist, the server MUST return the same error codes as defined + * for SSH_FXF_OPEN_EXISTING. + */ + public static final int SSH_FXF_TRUNCATE_EXISTING = 0x00000004; + + /** + * Data is always written at the end of the file. The offset field + * of the SSH_FXP_WRITE requests are ignored. + *

+ * Data is not required to be appended atomically. This means that + * if multiple writers attempt to append data simultaneously, data + * from the first may be lost. However, data MAY be appended + * atomically. + */ + public static final int SSH_FXF_ACCESS_APPEND_DATA = 0x00000008; + + /** + * Data is always written at the end of the file. The offset field + * of the SSH_FXP_WRITE requests are ignored. + *

+ * Data MUST be written atomically so that there is no chance that + * multiple appenders can collide and result in data being lost. + *

+ * If both append flags are specified, the server SHOULD use atomic + * append if it is available, but SHOULD use non-atomic appends + * otherwise. The server SHOULD NOT fail the request in this case. + */ + public static final int SSH_FXF_ACCESS_APPEND_DATA_ATOMIC = 0x00000010; + + /** + * Indicates that the server should treat the file as text and + * convert it to the canonical newline convention in use. + * (See Determining Server Newline Convention in section 5.3 in the + * SFTP standard draft). + *

+ * When a file is opened with this flag, the offset field in the read + * and write functions is ignored. + *

+ * Servers MUST process multiple, parallel reads and writes correctly + * in this mode. Naturally, it is permissible for them to do this by + * serializing the requests. + *

+ * Clients SHOULD use the SSH_FXF_ACCESS_APPEND_DATA flag to append + * data to a text file rather then using write with a calculated offset. + */ + public static final int SSH_FXF_ACCESS_TEXT_MODE = 0x00000020; + + /** + * The server MUST guarantee that no other handle has been opened + * with ACE4_READ_DATA access, and that no other handle will be + * opened with ACE4_READ_DATA access until the client closes the + * handle. (This MUST apply both to other clients and to other + * processes on the server.) + *

+ * If there is a conflicting lock the server MUST return + * SSH_FX_LOCK_CONFLICT. If the server cannot make the locking + * guarantee, it MUST return SSH_FX_OP_UNSUPPORTED. + *

+ * Other handles MAY be opened for ACE4_WRITE_DATA or any other + * combination of accesses, as long as ACE4_READ_DATA is not included + * in the mask. + */ + public static final int SSH_FXF_ACCESS_BLOCK_READ = 0x00000040; + + /** + * The server MUST guarantee that no other handle has been opened + * with ACE4_WRITE_DATA or ACE4_APPEND_DATA access, and that no other + * handle will be opened with ACE4_WRITE_DATA or ACE4_APPEND_DATA + * access until the client closes the handle. (This MUST apply both + * to other clients and to other processes on the server.) + *

+ * If there is a conflicting lock the server MUST return + * SSH_FX_LOCK_CONFLICT. If the server cannot make the locking + * guarantee, it MUST return SSH_FX_OP_UNSUPPORTED. + *

+ * Other handles MAY be opened for ACE4_READ_DATA or any other + * combination of accesses, as long as neither ACE4_WRITE_DATA nor + * ACE4_APPEND_DATA are included in the mask. + */ + public static final int SSH_FXF_ACCESS_BLOCK_WRITE = 0x00000080; + + /** + * The server MUST guarantee that no other handle has been opened + * with ACE4_DELETE access, opened with the + * SSH_FXF_ACCESS_DELETE_ON_CLOSE flag set, and that no other handle + * will be opened with ACE4_DELETE access or with the + * SSH_FXF_ACCESS_DELETE_ON_CLOSE flag set, and that the file itself + * is not deleted in any other way until the client closes the handle. + *

+ * If there is a conflicting lock the server MUST return + * SSH_FX_LOCK_CONFLICT. If the server cannot make the locking + * guarantee, it MUST return SSH_FX_OP_UNSUPPORTED. + */ + public static final int SSH_FXF_ACCESS_BLOCK_DELETE = 0x00000100; + + /** + * If this bit is set, the above BLOCK modes are advisory. In advisory + * mode, only other accesses that specify a BLOCK mode need be + * considered when determining whether the BLOCK can be granted, + * and the server need not prevent I/O operations that violate the + * block mode. + *

+ * The server MAY perform mandatory locking even if the BLOCK_ADVISORY + * bit is set. + */ + public static final int SSH_FXF_ACCESS_BLOCK_ADVISORY = 0x00000200; + + /** + * If the final component of the path is a symlink, then the open + * MUST fail, and the error SSH_FX_LINK_LOOP MUST be returned. + */ + public static final int SSH_FXF_ACCESS_NOFOLLOW = 0x00000400; + + /** + * The file should be deleted when the last handle to it is closed. + * (The last handle may not be an sftp-handle.) This MAY be emulated + * by a server if the OS doesn't support it by deleting the file when + * this handle is closed. + *

+ * It is implementation specific whether the directory entry is + * removed immediately or when the handle is closed. + */ + public static final int SSH_FXF_ACCESS_DELETE_ON_CLOSE = 0x00000800; +} diff --git a/app/src/main/java/com/trilead/ssh2/sftp/Packet.java b/app/src/main/java/com/trilead/ssh2/sftp/Packet.java new file mode 100644 index 0000000..444af90 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/sftp/Packet.java @@ -0,0 +1,43 @@ + +package com.trilead.ssh2.sftp; + +/** + * + * SFTP Paket Types + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Packet.java,v 1.1 2007/10/15 12:49:55 cplattne Exp $ + * + */ +public class Packet +{ + public static final int SSH_FXP_INIT = 1; + public static final int SSH_FXP_VERSION = 2; + public static final int SSH_FXP_OPEN = 3; + public static final int SSH_FXP_CLOSE = 4; + public static final int SSH_FXP_READ = 5; + public static final int SSH_FXP_WRITE = 6; + public static final int SSH_FXP_LSTAT = 7; + public static final int SSH_FXP_FSTAT = 8; + public static final int SSH_FXP_SETSTAT = 9; + public static final int SSH_FXP_FSETSTAT = 10; + public static final int SSH_FXP_OPENDIR = 11; + public static final int SSH_FXP_READDIR = 12; + public static final int SSH_FXP_REMOVE = 13; + public static final int SSH_FXP_MKDIR = 14; + public static final int SSH_FXP_RMDIR = 15; + public static final int SSH_FXP_REALPATH = 16; + public static final int SSH_FXP_STAT = 17; + public static final int SSH_FXP_RENAME = 18; + public static final int SSH_FXP_READLINK = 19; + public static final int SSH_FXP_SYMLINK = 20; + + public static final int SSH_FXP_STATUS = 101; + public static final int SSH_FXP_HANDLE = 102; + public static final int SSH_FXP_DATA = 103; + public static final int SSH_FXP_NAME = 104; + public static final int SSH_FXP_ATTRS = 105; + + public static final int SSH_FXP_EXTENDED = 200; + public static final int SSH_FXP_EXTENDED_REPLY = 201; +} diff --git a/app/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java b/app/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java new file mode 100644 index 0000000..6fb6ddb --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java @@ -0,0 +1,255 @@ + +package com.trilead.ssh2.signature; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + + +/** + * DSASHA1Verify. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: DSASHA1Verify.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class DSASHA1Verify +{ + private static final Logger log = Logger.getLogger(DSASHA1Verify.class); + + public static DSAPublicKey decodeSSHDSAPublicKey(byte[] key) throws IOException + { + TypesReader tr = new TypesReader(key); + + String key_format = tr.readString(); + + if (key_format.equals("ssh-dss") == false) + throw new IllegalArgumentException("This is not a ssh-dss public key!"); + + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + BigInteger g = tr.readMPINT(); + BigInteger y = tr.readMPINT(); + + if (tr.remain() != 0) + throw new IOException("Padding in DSA public key!"); + + try { + KeyFactory kf = KeyFactory.getInstance("DSA"); + + KeySpec ks = new DSAPublicKeySpec(y, p, q, g); + return (DSAPublicKey) kf.generatePublic(ks); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (InvalidKeySpecException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + public static byte[] encodeSSHDSAPublicKey(DSAPublicKey pk) throws IOException + { + TypesWriter tw = new TypesWriter(); + + tw.writeString("ssh-dss"); + + DSAParams params = pk.getParams(); + tw.writeMPInt(params.getP()); + tw.writeMPInt(params.getQ()); + tw.writeMPInt(params.getG()); + tw.writeMPInt(pk.getY()); + + return tw.getBytes(); + } + + /** + * Convert from Java's signature ASN.1 encoding to the SSH spec. + *

+ * Java ASN.1 encoding: + *

+	 * SEQUENCE ::= {
+	 *    r INTEGER,
+	 *    s INTEGER
+	 * }
+	 * 
+ */ + public static byte[] encodeSSHDSASignature(byte[] ds) + { + TypesWriter tw = new TypesWriter(); + + tw.writeString("ssh-dss"); + + int len, index; + + index = 3; + len = ds[index++] & 0xff; + byte[] r = new byte[len]; + System.arraycopy(ds, index, r, 0, r.length); + + index = index + len + 1; + len = ds[index++] & 0xff; + byte[] s = new byte[len]; + System.arraycopy(ds, index, s, 0, s.length); + + byte[] a40 = new byte[40]; + + /* Patch (unsigned) r and s into the target array. */ + + int r_copylen = (r.length < 20) ? r.length : 20; + int s_copylen = (s.length < 20) ? s.length : 20; + + System.arraycopy(r, r.length - r_copylen, a40, 20 - r_copylen, r_copylen); + System.arraycopy(s, s.length - s_copylen, a40, 40 - s_copylen, s_copylen); + + tw.writeString(a40, 0, 40); + + return tw.getBytes(); + } + + public static byte[] decodeSSHDSASignature(byte[] sig) throws IOException + { + byte[] rsArray = null; + + if (sig.length == 40) + { + /* OK, another broken SSH server. */ + rsArray = sig; + } + else + { + /* Hopefully a server obeying the standard... */ + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + if (sig_format.equals("ssh-dss") == false) + throw new IOException("Peer sent wrong signature format"); + + rsArray = tr.readByteString(); + + if (rsArray.length != 40) + throw new IOException("Peer sent corrupt signature"); + + if (tr.remain() != 0) + throw new IOException("Padding in DSA signature!"); + } + + int i = 0; + int j = 0; + byte[] tmp; + + if (rsArray[0] == 0 && rsArray[1] == 0 && rsArray[2] == 0) { + j = ((rsArray[i++] << 24) & 0xff000000) | ((rsArray[i++] << 16) & 0x00ff0000) + | ((rsArray[i++] << 8) & 0x0000ff00) | ((rsArray[i++]) & 0x000000ff); + i += j; + j = ((rsArray[i++] << 24) & 0xff000000) | ((rsArray[i++] << 16) & 0x00ff0000) + | ((rsArray[i++] << 8) & 0x0000ff00) | ((rsArray[i++]) & 0x000000ff); + tmp = new byte[j]; + System.arraycopy(rsArray, i, tmp, 0, j); + rsArray = tmp; + } + + /* ASN.1 */ + int frst = ((rsArray[0] & 0x80) != 0 ? 1 : 0); + int scnd = ((rsArray[20] & 0x80) != 0 ? 1 : 0); + + /* Calculate output length */ + int length = rsArray.length + 6 + frst + scnd; + tmp = new byte[length]; + + /* DER-encoding to match Java */ + tmp[0] = (byte) 0x30; + + if (rsArray.length != 40) + throw new IOException("Peer sent corrupt signature"); + /* Calculate length */ + tmp[1] = (byte) 0x2c; + tmp[1] += frst; + tmp[1] += scnd; + + /* First item */ + tmp[2] = (byte) 0x02; + + /* First item length */ + tmp[3] = (byte) 0x14; + tmp[3] += frst; + + /* Copy in the data for first item */ + System.arraycopy(rsArray, 0, tmp, 4 + frst, 20); + + /* Second item */ + tmp[4 + tmp[3]] = (byte) 0x02; + + /* Second item length */ + tmp[5 + tmp[3]] = (byte) 0x14; + tmp[5 + tmp[3]] += scnd; + + /* Copy in the data for the second item */ + System.arraycopy(rsArray, 20, tmp, 6 + tmp[3] + scnd, 20); + + /* Swap buffers */ + rsArray = tmp; + + return rsArray; + } + + public static boolean verifySignature(byte[] message, byte[] ds, DSAPublicKey dpk) throws IOException + { + try { + Signature s = Signature.getInstance("SHA1withDSA"); + s.initVerify(dpk); + s.update(message); + return s.verify(ds); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException("No such algorithm"); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + IOException ex = new IOException("No such algorithm"); + ex.initCause(e); + throw ex; + } catch (SignatureException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + public static byte[] generateSignature(byte[] message, DSAPrivateKey pk, SecureRandom rnd) throws IOException + { + try { + Signature s = Signature.getInstance("SHA1withDSA"); + s.initSign(pk); + s.update(message); + return s.sign(); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (SignatureException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java b/app/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java new file mode 100644 index 0000000..f139cdf --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java @@ -0,0 +1,487 @@ +/** + * + */ +package com.trilead.ssh2.signature; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Map; +import java.util.TreeMap; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + +/** + * @author Kenny Root + * + */ +public class ECDSASHA2Verify { + private static final Logger log = Logger.getLogger(ECDSASHA2Verify.class); + + public static final String ECDSA_SHA2_PREFIX = "ecdsa-sha2-"; + + private static final String NISTP256 = "nistp256"; + private static final String NISTP256_OID = "1.2.840.10045.3.1.7"; + private static final String NISTP384 = "nistp384"; + private static final String NISTP384_OID = "1.3.132.0.34"; + private static final String NISTP521 = "nistp521"; + private static final String NISTP521_OID = "1.3.132.0.35"; + + private static final Map CURVES = new TreeMap(); + static { + CURVES.put(NISTP256, EllipticCurves.nistp256); + CURVES.put(NISTP384, EllipticCurves.nistp384); + CURVES.put(NISTP521, EllipticCurves.nistp521); + } + + private static final Map CURVE_SIZES = new TreeMap(); + static { + CURVE_SIZES.put(256, NISTP256); + CURVE_SIZES.put(384, NISTP384); + CURVE_SIZES.put(521, NISTP521); + } + + private static final Map CURVE_OIDS = new TreeMap(); + static { + CURVE_OIDS.put(NISTP256_OID, NISTP256); + CURVE_OIDS.put(NISTP384_OID, NISTP256); + CURVE_OIDS.put(NISTP521_OID, NISTP256); + } + + public static int[] getCurveSizes() { + int[] keys = new int[CURVE_SIZES.size()]; + int i = 0; + for (Integer n : CURVE_SIZES.keySet().toArray(new Integer[keys.length])) { + keys[i++] = n; + } + return keys; + } + + public static ECParameterSpec getCurveForSize(int size) { + final String name = CURVE_SIZES.get(size); + if (name == null) { + return null; + } + return CURVES.get(name); + } + + public static ECPublicKey decodeSSHECDSAPublicKey(byte[] key) throws IOException + { + TypesReader tr = new TypesReader(key); + + String key_format = tr.readString(); + + if (key_format.startsWith(ECDSA_SHA2_PREFIX) == false) + throw new IllegalArgumentException("This is not an ECDSA public key"); + + String curveName = tr.readString(); + byte[] groupBytes = tr.readByteString(); + + if (tr.remain() != 0) + throw new IOException("Padding in ECDSA public key!"); + + if (key_format.equals(ECDSA_SHA2_PREFIX + curveName) == false) { + throw new IOException("Key format is inconsistent with curve name: " + key_format + + " != " + curveName); + } + + ECParameterSpec params = CURVES.get(curveName); + if (params == null) { + throw new IOException("Curve is not supported: " + curveName); + } + + ECPoint group = ECDSASHA2Verify.decodeECPoint(groupBytes, params.getCurve()); + if (group == null) { + throw new IOException("Invalid ECDSA group"); + } + + KeySpec keySpec = new ECPublicKeySpec(group, params); + + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + return (ECPublicKey) kf.generatePublic(keySpec); + } catch (NoSuchAlgorithmException nsae) { + IOException ioe = new IOException("No EC KeyFactory available"); + ioe.initCause(nsae); + throw ioe; + } catch (InvalidKeySpecException ikse) { + IOException ioe = new IOException("No EC KeyFactory available"); + ioe.initCause(ikse); + throw ioe; + } + } + + public static byte[] encodeSSHECDSAPublicKey(ECPublicKey key) throws IOException { + TypesWriter tw = new TypesWriter(); + + String curveName = getCurveName(key.getParams()); + + String keyFormat = ECDSA_SHA2_PREFIX + curveName; + + tw.writeString(keyFormat); + + tw.writeString(curveName); + + byte[] encoded = encodeECPoint(key.getW(), key.getParams().getCurve()); + tw.writeString(encoded, 0, encoded.length); + + return tw.getBytes(); + } + + public static String getCurveName(ECParameterSpec params) throws IOException { + int fieldSize = getCurveSize(params); + final String curveName = getCurveName(fieldSize); + if (curveName == null) { + throw new IOException("invalid curve size " + fieldSize); + } + return curveName; + } + + public static String getCurveName(int fieldSize) { + String curveName = CURVE_SIZES.get(fieldSize); + if (curveName == null) { + return null; + } + return curveName; + } + + public static int getCurveSize(ECParameterSpec params) { + return params.getCurve().getField().getFieldSize(); + } + + public static ECParameterSpec getCurveForOID(String oid) { + String name = CURVE_OIDS.get(oid); + if (name == null) + return null; + return CURVES.get(name); + } + + public static byte[] decodeSSHECDSASignature(byte[] sig) throws IOException { + byte[] rsArray = null; + + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + if (sig_format.startsWith(ECDSA_SHA2_PREFIX) == false) + throw new IOException("Peer sent wrong signature format"); + + String curveName = sig_format.substring(ECDSA_SHA2_PREFIX.length()); + if (CURVES.containsKey(curveName) == false) { + throw new IOException("Unsupported curve: " + curveName); + } + + rsArray = tr.readByteString(); + + if (tr.remain() != 0) + throw new IOException("Padding in ECDSA signature!"); + + byte[] rArray; + byte[] sArray; + { + TypesReader rsReader = new TypesReader(rsArray); + rArray = rsReader.readMPINT().toByteArray(); + sArray = rsReader.readMPINT().toByteArray(); + } + + int first = rArray.length; + int second = sArray.length; + + /* We can't have the high bit set, so add an extra zero at the beginning if so. */ + if ((rArray[0] & 0x80) != 0) { + first++; + } + if ((sArray[0] & 0x80) != 0) { + second++; + } + + /* Calculate total output length */ + ByteArrayOutputStream os = new ByteArrayOutputStream(6 + first + second); + + /* ASN.1 SEQUENCE tag */ + os.write(0x30); + + /* Size of SEQUENCE */ + writeLength(4 + first + second, os); + + /* ASN.1 INTEGER tag */ + os.write(0x02); + + /* "r" INTEGER length */ + writeLength(first, os); + + /* Copy in the "r" INTEGER */ + if (first != rArray.length) { + os.write(0x00); + } + os.write(rArray); + + /* ASN.1 INTEGER tag */ + os.write(0x02); + + /* "s" INTEGER length */ + writeLength(second, os); + + /* Copy in the "s" INTEGER */ + if (second != sArray.length) { + os.write(0x00); + } + os.write(sArray); + + return os.toByteArray(); + } + + private static final void writeLength(int length, OutputStream os) throws IOException { + if (length <= 0x7F) { + os.write(length); + return; + } + + int numOctets = 0; + int lenCopy = length; + while (lenCopy != 0) { + lenCopy >>>= 8; + numOctets++; + } + + os.write(0x80 | numOctets); + + for (int i = (numOctets - 1) * 8; i >= 0; i -= 8) { + os.write((byte) (length >> i)); + } + } + + public static byte[] encodeSSHECDSASignature(byte[] sig, ECParameterSpec params) throws IOException + { + TypesWriter tw = new TypesWriter(); + + String curveName = getCurveName(params); + tw.writeString(ECDSA_SHA2_PREFIX + curveName); + + if ((sig[0] != 0x30) || (sig[1] != sig.length - 2) || (sig[2] != 0x02)) { + throw new IOException("Invalid signature format"); + } + + int rLength = sig[3]; + if ((rLength + 6 > sig.length) || (sig[4 + rLength] != 0x02)) { + throw new IOException("Invalid signature format"); + } + + int sLength = sig[5 + rLength]; + if (6 + rLength + sLength > sig.length) { + throw new IOException("Invalid signature format"); + } + + byte[] rArray = new byte[rLength]; + byte[] sArray = new byte[sLength]; + + System.arraycopy(sig, 4, rArray, 0, rLength); + System.arraycopy(sig, 6 + rLength, sArray, 0, sLength); + + BigInteger r = new BigInteger(rArray); + BigInteger s = new BigInteger(sArray); + + // Write the to its own types writer. + TypesWriter rsWriter = new TypesWriter(); + rsWriter.writeMPInt(r); + rsWriter.writeMPInt(s); + byte[] encoded = rsWriter.getBytes(); + tw.writeString(encoded, 0, encoded.length); + + return tw.getBytes(); + } + + public static byte[] generateSignature(byte[] message, ECPrivateKey pk) throws IOException + { + final String algo = getSignatureAlgorithmForParams(pk.getParams()); + + try { + Signature s = Signature.getInstance(algo); + s.initSign(pk); + s.update(message); + return s.sign(); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (SignatureException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + public static boolean verifySignature(byte[] message, byte[] ds, ECPublicKey dpk) throws IOException + { + final String algo = getSignatureAlgorithmForParams(dpk.getParams()); + + try { + Signature s = Signature.getInstance(algo); + s.initVerify(dpk); + s.update(message); + return s.verify(ds); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException("No such algorithm"); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + IOException ex = new IOException("No such algorithm"); + ex.initCause(e); + throw ex; + } catch (SignatureException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + private static String getSignatureAlgorithmForParams(ECParameterSpec params) { + int size = getCurveSize(params); + if (size <= 256) { + return "SHA256withECDSA"; + } else if (size <= 384) { + return "SHA384withECDSA"; + } else { + return "SHA512withECDSA"; + } + } + + public static String getDigestAlgorithmForParams(ECParameterSpec params) { + int size = getCurveSize(params); + if (size <= 256) { + return "SHA256"; + } else if (size <= 384) { + return "SHA384"; + } else { + return "SHA512"; + } + } + + /** + * Decode an OctetString to EllipticCurvePoint according to SECG 2.3.4 + */ + public static ECPoint decodeECPoint(byte[] M, EllipticCurve curve) { + if (M.length == 0) { + return null; + } + + // M has len 2 ceil(log_2(q)/8) + 1 ? + int elementSize = (curve.getField().getFieldSize() + 7) / 8; + if (M.length != 2 * elementSize + 1) { + return null; + } + + // step 3.2 + if (M[0] != 0x04) { + return null; + } + + // Step 3.3 + byte[] xp = new byte[elementSize]; + System.arraycopy(M, 1, xp, 0, elementSize); + + // Step 3.4 + byte[] yp = new byte[elementSize]; + System.arraycopy(M, 1 + elementSize, yp, 0, elementSize); + + ECPoint P = new ECPoint(new BigInteger(1, xp), new BigInteger(1, yp)); + + // TODO check point 3.5 + + // Step 3.6 + return P; + } + + /** + * Encode EllipticCurvePoint to an OctetString + */ + public static byte[] encodeECPoint(ECPoint group, EllipticCurve curve) + { + // M has len 2 ceil(log_2(q)/8) + 1 ? + int elementSize = (curve.getField().getFieldSize() + 7) / 8; + byte[] M = new byte[2 * elementSize + 1]; + + // Uncompressed format + M[0] = 0x04; + + { + byte[] affineX = removeLeadingZeroes(group.getAffineX().toByteArray()); + System.arraycopy(affineX, 0, M, 1 + elementSize - affineX.length, affineX.length); + } + + { + byte[] affineY = removeLeadingZeroes(group.getAffineY().toByteArray()); + System.arraycopy(affineY, 0, M, 1 + elementSize + elementSize - affineY.length, + affineY.length); + } + + return M; + } + + private static byte[] removeLeadingZeroes(byte[] input) { + if (input[0] != 0x00) { + return input; + } + + int pos = 1; + while (pos < input.length - 1 && input[pos] == 0x00) { + pos++; + } + + byte[] output = new byte[input.length - pos]; + System.arraycopy(input, pos, output, 0, output.length); + return output; + } + + public static class EllipticCurves { + public static ECParameterSpec nistp256 = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16)), + new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16), + new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16)), + new ECPoint(new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16), + new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16)), + new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16), + 1); + + public static ECParameterSpec nistp384 = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16), + new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16)), + new ECPoint(new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16), + new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16), + 1); + + public static ECParameterSpec nistp521 = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16)), + new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC", 16), + new BigInteger("0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00", 16)), + new ECPoint(new BigInteger("00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66", 16), + new BigInteger("011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650", 16)), + new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", 16), + 1); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java b/app/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java new file mode 100644 index 0000000..3406312 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java @@ -0,0 +1,180 @@ + +package com.trilead.ssh2.signature; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.RSAPublicKeySpec; + +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; + + +/** + * RSASHA1Verify. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: RSASHA1Verify.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class RSASHA1Verify +{ + private static final Logger log = Logger.getLogger(RSASHA1Verify.class); + + public static RSAPublicKey decodeSSHRSAPublicKey(byte[] key) throws IOException + { + TypesReader tr = new TypesReader(key); + + String key_format = tr.readString(); + + if (key_format.equals("ssh-rsa") == false) + throw new IllegalArgumentException("This is not a ssh-rsa public key"); + + BigInteger e = tr.readMPINT(); + BigInteger n = tr.readMPINT(); + + if (tr.remain() != 0) + throw new IOException("Padding in RSA public key!"); + + KeySpec keySpec = new RSAPublicKeySpec(n, e); + + try { + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) kf.generatePublic(keySpec); + } catch (NoSuchAlgorithmException nsae) { + IOException ioe = new IOException("No RSA KeyFactory available"); + ioe.initCause(nsae); + throw ioe; + } catch (InvalidKeySpecException ikse) { + IOException ioe = new IOException("No RSA KeyFactory available"); + ioe.initCause(ikse); + throw ioe; + } + } + + public static byte[] encodeSSHRSAPublicKey(RSAPublicKey pk) throws IOException + { + TypesWriter tw = new TypesWriter(); + + tw.writeString("ssh-rsa"); + tw.writeMPInt(pk.getPublicExponent()); + tw.writeMPInt(pk.getModulus()); + + return tw.getBytes(); + } + + public static byte[] decodeSSHRSASignature(byte[] sig) throws IOException + { + TypesReader tr = new TypesReader(sig); + + String sig_format = tr.readString(); + + if (sig_format.equals("ssh-rsa") == false) + throw new IOException("Peer sent wrong signature format"); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." See also below. + */ + + byte[] s = tr.readByteString(); + + if (s.length == 0) + throw new IOException("Error in RSA signature, S is empty."); + + if (log.isEnabled()) + { + log.log(80, "Decoding ssh-rsa signature string (length: " + s.length + ")"); + } + + if (tr.remain() != 0) + throw new IOException("Padding in RSA signature!"); + + if (s[0] == 0 && s[1] == 0 && s[2] == 0) { + int i = 0; + int j = ((s[i++] << 24) & 0xff000000) | ((s[i++] << 16) & 0x00ff0000) + | ((s[i++] << 8) & 0x0000ff00) | ((s[i++]) & 0x000000ff); + i += j; + j = ((s[i++] << 24) & 0xff000000) | ((s[i++] << 16) & 0x00ff0000) + | ((s[i++] << 8) & 0x0000ff00) | ((s[i++]) & 0x000000ff); + byte[] tmp = new byte[j]; + System.arraycopy(s, i, tmp, 0, j); + sig = tmp; + } + + return s; + } + + public static byte[] encodeSSHRSASignature(byte[] s) throws IOException + { + TypesWriter tw = new TypesWriter(); + + tw.writeString("ssh-rsa"); + + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string + * containing s (which is an integer, without lengths or padding, unsigned and in + * network byte order)." + */ + + /* Remove first zero sign byte, if present */ + + if ((s.length > 1) && (s[0] == 0x00)) + tw.writeString(s, 1, s.length - 1); + else + tw.writeString(s, 0, s.length); + + return tw.getBytes(); + } + + public static byte[] generateSignature(byte[] message, RSAPrivateKey pk) throws IOException + { + try { + Signature s = Signature.getInstance("SHA1withRSA"); + s.initSign(pk); + s.update(message); + return s.sign(); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (SignatureException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + public static boolean verifySignature(byte[] message, byte[] ds, RSAPublicKey dpk) throws IOException + { + try { + Signature s = Signature.getInstance("SHA1withRSA"); + s.initVerify(dpk); + s.update(message); + return s.verify(ds); + } catch (NoSuchAlgorithmException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (InvalidKeyException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } catch (SignatureException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java b/app/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java new file mode 100644 index 0000000..d7a5ee5 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java @@ -0,0 +1,125 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import com.trilead.ssh2.Connection; + +/** + * ClientServerHello. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ClientServerHello.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class ClientServerHello +{ + String server_line; + String client_line; + + String server_versioncomment; + + public final static int readLineRN(InputStream is, byte[] buffer) throws IOException + { + int pos = 0; + boolean need10 = false; + int len = 0; + while (true) + { + int c = is.read(); + if (c == -1) + throw new IOException("Premature connection close"); + + buffer[pos++] = (byte) c; + + if (c == 13) + { + need10 = true; + continue; + } + + if (c == 10) + break; + + if (need10 == true) + throw new IOException("Malformed line sent by the server, the line does not end correctly."); + + len++; + if (pos >= buffer.length) + throw new IOException("The server sent a too long line."); + } + + return len; + } + + public ClientServerHello(InputStream bi, OutputStream bo) throws IOException + { + client_line = "SSH-2.0-" + Connection.identification; + + bo.write((client_line + "\r\n").getBytes("ISO-8859-1")); + bo.flush(); + + byte[] serverVersion = new byte[512]; + + for (int i = 0; i < 50; i++) + { + int len = readLineRN(bi, serverVersion); + + server_line = new String(serverVersion, 0, len, "ISO-8859-1"); + + if (server_line.startsWith("SSH-")) + break; + } + + if (server_line.startsWith("SSH-") == false) + throw new IOException( + "Malformed server identification string. There was no line starting with 'SSH-' amongst the first 50 lines."); + + if (server_line.startsWith("SSH-1.99-")) + server_versioncomment = server_line.substring(9); + else if (server_line.startsWith("SSH-2.0-")) + server_versioncomment = server_line.substring(8); + else + throw new IOException("Server uses incompatible protocol, it is not SSH-2 compatible."); + } + + /** + * @return Returns the client_versioncomment. + */ + public byte[] getClientString() + { + byte[] result; + + try + { + result = client_line.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException ign) + { + result = client_line.getBytes(); + } + + return result; + } + + /** + * @return Returns the server_versioncomment. + */ + public byte[] getServerString() + { + byte[] result; + + try + { + result = server_line.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException ign) + { + result = server_line.getBytes(); + } + + return result; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/KexManager.java b/app/src/main/java/com/trilead/ssh2/transport/KexManager.java new file mode 100644 index 0000000..cd26530 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/KexManager.java @@ -0,0 +1,671 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.security.SecureRandom; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Set; +import java.util.TreeSet; + +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.compression.CompressionFactory; +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.KeyMaterial; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.dh.DhGroupExchange; +import com.trilead.ssh2.crypto.dh.GenericDhExchange; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketKexDHInit; +import com.trilead.ssh2.packets.PacketKexDHReply; +import com.trilead.ssh2.packets.PacketKexDhGexGroup; +import com.trilead.ssh2.packets.PacketKexDhGexInit; +import com.trilead.ssh2.packets.PacketKexDhGexReply; +import com.trilead.ssh2.packets.PacketKexDhGexRequest; +import com.trilead.ssh2.packets.PacketKexDhGexRequestOld; +import com.trilead.ssh2.packets.PacketKexInit; +import com.trilead.ssh2.packets.PacketNewKeys; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + + +/** + * KexManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexManager.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KexManager +{ + private static final Logger log = Logger.getLogger(KexManager.class); + + private static final Set HOSTKEY_ALGS = new TreeSet(); + static { + HOSTKEY_ALGS.add("ecdsa-sha2-nistp256"); + HOSTKEY_ALGS.add("ecdsa-sha2-nistp384"); + HOSTKEY_ALGS.add("ecdsa-sha2-nistp521"); + HOSTKEY_ALGS.add("ssh-rsa"); + HOSTKEY_ALGS.add("ssh-dsa"); + } + + private static final Set KEX_ALGS = new TreeSet(); + static { + KEX_ALGS.add("ecdh-sha2-nistp256"); + KEX_ALGS.add("ecdh-sha2-nistp384"); + KEX_ALGS.add("ecdh-sha2-nistp521"); + KEX_ALGS.add("diffie-hellman-group-exchange-sha256"); + KEX_ALGS.add("diffie-hellman-group-exchange-sha1"); + KEX_ALGS.add("diffie-hellman-group14-sha1"); + KEX_ALGS.add("diffie-hellman-group1-sha1"); + } + + KexState kxs; + int kexCount = 0; + KeyMaterial km; + byte[] sessionId; + ClientServerHello csh; + + final Object accessLock = new Object(); + ConnectionInfo lastConnInfo = null; + + boolean connectionClosed = false; + + boolean ignore_next_kex_packet = false; + + final TransportManager tm; + + CryptoWishList nextKEXcryptoWishList; + DHGexParameters nextKEXdhgexParameters; + + ServerHostKeyVerifier verifier; + final String hostname; + final int port; + final SecureRandom rnd; + + public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port, + ServerHostKeyVerifier keyVerifier, SecureRandom rnd) + { + this.tm = tm; + this.csh = csh; + this.nextKEXcryptoWishList = initialCwl; + this.nextKEXdhgexParameters = new DHGexParameters(); + this.hostname = hostname; + this.port = port; + this.verifier = keyVerifier; + this.rnd = rnd; + } + + public ConnectionInfo getOrWaitForConnectionInfo(int minKexCount) throws IOException + { + synchronized (accessLock) + { + while (true) + { + if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount)) + return lastConnInfo; + + if (connectionClosed) + throw (IOException) new IOException("Key exchange was not finished, connection is closed.") + .initCause(tm.getReasonClosedCause()); + + try + { + accessLock.wait(); + } + catch (InterruptedException e) + { + } + } + } + } + + private String getFirstMatch(String[] client, String[] server) throws NegotiateException + { + if (client == null || server == null) + throw new IllegalArgumentException(); + + if (client.length == 0) + return null; + + for (int i = 0; i < client.length; i++) + { + for (int j = 0; j < server.length; j++) + { + if (client[i].equals(server[j])) + return client[i]; + } + } + throw new NegotiateException(); + } + + private boolean compareFirstOfNameList(String[] a, String[] b) + { + if (a == null || b == null) + throw new IllegalArgumentException(); + + if ((a.length == 0) && (b.length == 0)) + return true; + + if ((a.length == 0) || (b.length == 0)) + return false; + + return (a[0].equals(b[0])); + } + + private boolean isGuessOK(KexParameters cpar, KexParameters spar) + { + if (cpar == null || spar == null) + throw new IllegalArgumentException(); + + if (compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms) == false) + { + return false; + } + + if (compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms) == false) + { + return false; + } + + /* + * We do NOT check here if the other algorithms can be agreed on, this + * is just a check if kex_algorithms and server_host_key_algorithms were + * guessed right! + */ + + return true; + } + + private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server) + { + NegotiatedParameters np = new NegotiatedParameters(); + + try + { + np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms); + + log.log(20, "kex_algo=" + np.kex_algo); + + np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms, + server.server_host_key_algorithms); + + log.log(20, "server_host_key_algo=" + np.server_host_key_algo); + + np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server, + server.encryption_algorithms_client_to_server); + np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client, + server.encryption_algorithms_server_to_client); + + log.log(20, "enc_algo_client_to_server=" + np.enc_algo_client_to_server); + log.log(20, "enc_algo_server_to_client=" + np.enc_algo_server_to_client); + + np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server, + server.mac_algorithms_client_to_server); + np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client, + server.mac_algorithms_server_to_client); + + log.log(20, "mac_algo_client_to_server=" + np.mac_algo_client_to_server); + log.log(20, "mac_algo_server_to_client=" + np.mac_algo_server_to_client); + + np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server, + server.compression_algorithms_client_to_server); + np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client, + server.compression_algorithms_server_to_client); + + log.log(20, "comp_algo_client_to_server=" + np.comp_algo_client_to_server); + log.log(20, "comp_algo_server_to_client=" + np.comp_algo_server_to_client); + + } + catch (NegotiateException e) + { + return null; + } + + try + { + np.lang_client_to_server = getFirstMatch(client.languages_client_to_server, + server.languages_client_to_server); + } + catch (NegotiateException e1) + { + np.lang_client_to_server = null; + } + + try + { + np.lang_server_to_client = getFirstMatch(client.languages_server_to_client, + server.languages_server_to_client); + } + catch (NegotiateException e2) + { + np.lang_server_to_client = null; + } + + if (isGuessOK(client, server)) + np.guessOK = true; + + return np; + } + + public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) throws IOException + { + nextKEXcryptoWishList = cwl; + nextKEXdhgexParameters = dhgex; + + if (kxs == null) + { + kxs = new KexState(); + + kxs.dhgexParameters = nextKEXdhgexParameters; + PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList); + kxs.localKEX = kp; + tm.sendKexMessage(kp.getPayload()); + } + } + + private boolean establishKeyMaterial() + { + try + { + int mac_cs_key_len = MAC.getKeyLen(kxs.np.mac_algo_client_to_server); + int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server); + int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server); + + int mac_sc_key_len = MAC.getKeyLen(kxs.np.mac_algo_server_to_client); + int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client); + int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client); + + km = KeyMaterial.create(kxs.hashAlgo, kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len, + enc_sc_key_len, enc_sc_block_len, mac_sc_key_len); + } + catch (IllegalArgumentException e) + { + return false; + } + return true; + } + + private void finishKex() throws IOException + { + if (sessionId == null) + sessionId = kxs.H; + + establishKeyMaterial(); + + /* Tell the other side that we start using the new material */ + + PacketNewKeys ign = new PacketNewKeys(); + tm.sendKexMessage(ign.getPayload()); + + BlockCipher cbc; + MAC mac; + ICompressor comp; + + try + { + cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server, + km.initial_iv_client_to_server); + + mac = new MAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server); + + comp = CompressionFactory.createCompressor(kxs.np.comp_algo_client_to_server); + + } + catch (IllegalArgumentException e1) + { + throw new IOException("Fatal error during MAC startup!"); + } + + tm.changeSendCipher(cbc, mac); + tm.changeSendCompression(comp); + tm.kexFinished(); + } + + public static final String[] getDefaultServerHostkeyAlgorithmList() + { + return HOSTKEY_ALGS.toArray(new String[HOSTKEY_ALGS.size()]); + } + + public static final void checkServerHostkeyAlgorithmsList(String[] algos) + { + for (int i = 0; i < algos.length; i++) + { + if (!HOSTKEY_ALGS.contains(algos[i])) + throw new IllegalArgumentException("Unknown server host key algorithm '" + algos[i] + "'"); + } + } + + public static final String[] getDefaultKexAlgorithmList() + { + return KEX_ALGS.toArray(new String[KEX_ALGS.size()]); + } + + public static final void checkKexAlgorithmList(String[] algos) + { + for (int i = 0; i < algos.length; i++) + { + if (!KEX_ALGS.contains(algos[i])) + throw new IllegalArgumentException("Unknown kex algorithm '" + algos[i] + "'"); + } + } + + private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException + { + if (kxs.np.server_host_key_algo.startsWith("ecdsa-sha2-")) + { + byte[] rs = ECDSASHA2Verify.decodeSSHECDSASignature(sig); + ECPublicKey epk = ECDSASHA2Verify.decodeSSHECDSAPublicKey(hostkey); + + log.log(50, "Verifying ecdsa signature"); + + return ECDSASHA2Verify.verifySignature(kxs.H, rs, epk); + } + + if (kxs.np.server_host_key_algo.equals("ssh-rsa")) + { + byte[] rs = RSASHA1Verify.decodeSSHRSASignature(sig); + RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey); + + log.log(50, "Verifying ssh-rsa signature"); + + return RSASHA1Verify.verifySignature(kxs.H, rs, rpk); + } + + if (kxs.np.server_host_key_algo.equals("ssh-dss")) + { + byte[] ds = DSASHA1Verify.decodeSSHDSASignature(sig); + DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(hostkey); + + log.log(50, "Verifying ssh-dss signature"); + + return DSASHA1Verify.verifySignature(kxs.H, ds, dpk); + } + + throw new IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'"); + } + + public synchronized void handleMessage(byte[] msg, int msglen) throws IOException + { + PacketKexInit kip; + + if (msg == null) + { + synchronized (accessLock) + { + connectionClosed = true; + accessLock.notifyAll(); + return; + } + } + + if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT)) + throw new IOException("Unexpected KEX message (type " + msg[0] + ")"); + + if (ignore_next_kex_packet) + { + ignore_next_kex_packet = false; + return; + } + + if (msg[0] == Packets.SSH_MSG_KEXINIT) + { + if ((kxs != null) && (kxs.state != 0)) + throw new IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!"); + + if (kxs == null) + { + /* + * Ah, OK, peer wants to do KEX. Let's be nice and play + * together. + */ + kxs = new KexState(); + kxs.dhgexParameters = nextKEXdhgexParameters; + kip = new PacketKexInit(nextKEXcryptoWishList); + kxs.localKEX = kip; + tm.sendKexMessage(kip.getPayload()); + } + + kip = new PacketKexInit(msg, 0, msglen); + kxs.remoteKEX = kip; + + kxs.np = mergeKexParameters(kxs.localKEX.getKexParameters(), kxs.remoteKEX.getKexParameters()); + + if (kxs.np == null) + throw new IOException("Cannot negotiate, proposals do not match."); + + if (kxs.remoteKEX.isFirst_kex_packet_follows() && (kxs.np.guessOK == false)) + { + /* + * Guess was wrong, we need to ignore the next kex packet. + */ + + ignore_next_kex_packet = true; + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) + { + if (kxs.dhgexParameters.getMin_group_len() == 0 || csh.server_versioncomment.matches("OpenSSH_2\\.([0-4]\\.|5\\.[0-2]).*")) + { + PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters); + tm.sendKexMessage(dhgexreq.getPayload()); + } + else + { + PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters); + tm.sendKexMessage(dhgexreq.getPayload()); + } + if (kxs.np.kex_algo.endsWith("sha1")) { + kxs.hashAlgo = "SHA1"; + } else { + kxs.hashAlgo = "SHA-256"; + } + kxs.state = 1; + return; + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp256") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp384") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp521")) { + kxs.dhx = GenericDhExchange.getInstance(kxs.np.kex_algo); + + kxs.dhx.init(kxs.np.kex_algo); + kxs.hashAlgo = kxs.dhx.getHashAlgo(); + + PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.getE()); + tm.sendKexMessage(kp.getPayload()); + kxs.state = 1; + return; + } + + throw new IllegalStateException("Unknown KEX method!"); + } + + if (msg[0] == Packets.SSH_MSG_NEWKEYS) + { + if (km == null) + throw new IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!"); + + BlockCipher cbc; + MAC mac; + ICompressor comp; + + try + { + cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false, + km.enc_key_server_to_client, km.initial_iv_server_to_client); + + mac = new MAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client); + + comp = CompressionFactory.createCompressor(kxs.np.comp_algo_server_to_client); + } + catch (IllegalArgumentException e1) + { + throw new IOException("Fatal error during MAC startup!"); + } + + tm.changeRecvCipher(cbc, mac); + tm.changeRecvCompression(comp); + + ConnectionInfo sci = new ConnectionInfo(); + + kexCount++; + + sci.keyExchangeAlgorithm = kxs.np.kex_algo; + sci.keyExchangeCounter = kexCount; + sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server; + sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client; + sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server; + sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client; + sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo; + sci.serverHostKey = kxs.hostkey; + + synchronized (accessLock) + { + lastConnInfo = sci; + accessLock.notifyAll(); + } + + kxs = null; + return; + } + + if ((kxs == null) || (kxs.state == 0)) + throw new IOException("Unexpected Kex submessage!"); + + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) + { + if (kxs.state == 1) + { + PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen); + kxs.dhgx = new DhGroupExchange(dhgexgrp.getP(), dhgexgrp.getG()); + kxs.dhgx.init(rnd); + PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.getE()); + tm.sendKexMessage(dhgexinit.getPayload()); + kxs.state = 2; + return; + } + + if (kxs.state == 2) + { + PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen); + + kxs.hostkey = dhgexrpl.getHostKey(); + + if (verifier != null) + { + boolean vres = false; + + try + { + vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + } + catch (Exception e) + { + throw (IOException) new IOException( + "The server hostkey was not accepted by the verifier callback.").initCause(e); + } + + if (vres == false) + throw new IOException("The server hostkey was not accepted by the verifier callback"); + } + + kxs.dhgx.setF(dhgexrpl.getF()); + + try + { + kxs.H = kxs.dhgx.calculateH(kxs.hashAlgo, + csh.getClientString(), csh.getServerString(), + kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(), + dhgexrpl.getHostKey(), kxs.dhgexParameters); + } + catch (IllegalArgumentException e) + { + throw (IOException) new IOException("KEX error.").initCause(e); + } + + boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey); + + if (res == false) + throw new IOException("Hostkey signature sent by remote is wrong!"); + + kxs.K = kxs.dhgx.getK(); + + finishKex(); + kxs.state = -1; + return; + } + + throw new IllegalStateException("Illegal State in KEX Exchange!"); + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp256") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp384") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp521")) + { + if (kxs.state == 1) + { + + PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen); + + kxs.hostkey = dhr.getHostKey(); + + if (verifier != null) + { + boolean vres = false; + + try + { + vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + } + catch (Exception e) + { + throw (IOException) new IOException( + "The server hostkey was not accepted by the verifier callback.").initCause(e); + } + + if (vres == false) + throw new IOException("The server hostkey was not accepted by the verifier callback"); + } + + kxs.dhx.setF(dhr.getF()); + + try + { + kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(), + kxs.remoteKEX.getPayload(), dhr.getHostKey()); + } + catch (IllegalArgumentException e) + { + throw (IOException) new IOException("KEX error.").initCause(e); + } + + boolean res = verifySignature(dhr.getSignature(), kxs.hostkey); + + if (res == false) + throw new IOException("Hostkey signature sent by remote is wrong!"); + + kxs.K = kxs.dhx.getK(); + + finishKex(); + kxs.state = -1; + return; + } + } + + throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")"); + } +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/KexParameters.java b/app/src/main/java/com/trilead/ssh2/transport/KexParameters.java new file mode 100644 index 0000000..70bcf3e --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/KexParameters.java @@ -0,0 +1,24 @@ +package com.trilead.ssh2.transport; + +/** + * KexParameters. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexParameters.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KexParameters +{ + public byte[] cookie; + public String[] kex_algorithms; + public String[] server_host_key_algorithms; + public String[] encryption_algorithms_client_to_server; + public String[] encryption_algorithms_server_to_client; + public String[] mac_algorithms_client_to_server; + public String[] mac_algorithms_server_to_client; + public String[] compression_algorithms_client_to_server; + public String[] compression_algorithms_server_to_client; + public String[] languages_client_to_server; + public String[] languages_server_to_client; + public boolean first_kex_packet_follows; + public int reserved_field1; +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/KexState.java b/app/src/main/java/com/trilead/ssh2/transport/KexState.java new file mode 100644 index 0000000..8611f3f --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/KexState.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.transport; + + +import java.math.BigInteger; + +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.crypto.dh.DhGroupExchange; +import com.trilead.ssh2.crypto.dh.GenericDhExchange; +import com.trilead.ssh2.packets.PacketKexInit; + +/** + * KexState. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexState.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class KexState +{ + public PacketKexInit localKEX; + public PacketKexInit remoteKEX; + public NegotiatedParameters np; + public int state = 0; + + public BigInteger K; + public byte[] H; + + public byte[] hostkey; + + public String hashAlgo; + public GenericDhExchange dhx; + public DhGroupExchange dhgx; + public DHGexParameters dhgexParameters; +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/MessageHandler.java b/app/src/main/java/com/trilead/ssh2/transport/MessageHandler.java new file mode 100644 index 0000000..039d473 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/MessageHandler.java @@ -0,0 +1,14 @@ +package com.trilead.ssh2.transport; + +import java.io.IOException; + +/** + * MessageHandler. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: MessageHandler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public interface MessageHandler +{ + public void handleMessage(byte[] msg, int msglen) throws IOException; +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/NegotiateException.java b/app/src/main/java/com/trilead/ssh2/transport/NegotiateException.java new file mode 100644 index 0000000..ff53097 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/NegotiateException.java @@ -0,0 +1,12 @@ +package com.trilead.ssh2.transport; + +/** + * NegotiateException. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NegotiateException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class NegotiateException extends Exception +{ + private static final long serialVersionUID = 3689910669428143157L; +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java b/app/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java new file mode 100644 index 0000000..e9f3a0a --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java @@ -0,0 +1,22 @@ +package com.trilead.ssh2.transport; + +/** + * NegotiatedParameters. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NegotiatedParameters.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class NegotiatedParameters +{ + public boolean guessOK; + public String kex_algo; + public String server_host_key_algo; + public String enc_algo_client_to_server; + public String enc_algo_server_to_client; + public String mac_algo_client_to_server; + public String mac_algo_server_to_client; + public String comp_algo_client_to_server; + public String comp_algo_server_to_client; + public String lang_client_to_server; + public String lang_server_to_client; +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/TransportConnection.java b/app/src/main/java/com/trilead/ssh2/transport/TransportConnection.java new file mode 100644 index 0000000..906c3c9 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/TransportConnection.java @@ -0,0 +1,343 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; + +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.CipherInputStream; +import com.trilead.ssh2.crypto.cipher.CipherOutputStream; +import com.trilead.ssh2.crypto.cipher.NullCipher; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.Packets; + + +/** + * TransportConnection. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TransportConnection.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class TransportConnection +{ + private static final Logger log = Logger.getLogger(TransportConnection.class); + + int send_seq_number = 0; + + int recv_seq_number = 0; + + CipherInputStream cis; + + CipherOutputStream cos; + + boolean useRandomPadding = false; + + /* Depends on current MAC and CIPHER */ + + MAC send_mac; + + byte[] send_mac_buffer; + + int send_padd_blocksize = 8; + + MAC recv_mac; + + byte[] recv_mac_buffer; + + byte[] recv_mac_buffer_cmp; + + int recv_padd_blocksize = 8; + + ICompressor recv_comp = null; + + ICompressor send_comp = null; + + boolean can_recv_compress = false; + + boolean can_send_compress = false; + + byte[] recv_comp_buffer; + + byte[] send_comp_buffer; + + /* won't change */ + + final byte[] send_padding_buffer = new byte[256]; + + final byte[] send_packet_header_buffer = new byte[5]; + + final byte[] recv_padding_buffer = new byte[256]; + + final byte[] recv_packet_header_buffer = new byte[5]; + + boolean recv_packet_header_present = false; + + ClientServerHello csh; + + final SecureRandom rnd; + + public TransportConnection(InputStream is, OutputStream os, SecureRandom rnd) + { + this.cis = new CipherInputStream(new NullCipher(), is); + this.cos = new CipherOutputStream(new NullCipher(), os); + this.rnd = rnd; + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + cis.changeCipher(bc); + recv_mac = mac; + recv_mac_buffer = (mac != null) ? new byte[mac.size()] : null; + recv_mac_buffer_cmp = (mac != null) ? new byte[mac.size()] : null; + recv_padd_blocksize = bc.getBlockSize(); + if (recv_padd_blocksize < 8) + recv_padd_blocksize = 8; + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + if ((bc instanceof NullCipher) == false) + { + /* Only use zero byte padding for the first few packets */ + useRandomPadding = true; + /* Once we start encrypting, there is no way back */ + } + + cos.changeCipher(bc); + send_mac = mac; + send_mac_buffer = (mac != null) ? new byte[mac.size()] : null; + send_padd_blocksize = bc.getBlockSize(); + if (send_padd_blocksize < 8) + send_padd_blocksize = 8; + } + + public void changeRecvCompression(ICompressor comp) + { + recv_comp = comp; + + if (comp != null) { + recv_comp_buffer = new byte[comp.getBufferSize()]; + can_recv_compress |= recv_comp.canCompressPreauth(); + } + } + + public void changeSendCompression(ICompressor comp) + { + send_comp = comp; + + if (comp != null) { + send_comp_buffer = new byte[comp.getBufferSize()]; + can_send_compress |= send_comp.canCompressPreauth(); + } + } + + public void sendMessage(byte[] message) throws IOException + { + sendMessage(message, 0, message.length, 0); + } + + public void sendMessage(byte[] message, int off, int len) throws IOException + { + sendMessage(message, off, len, 0); + } + + public int getPacketOverheadEstimate() + { + // return an estimate for the paket overhead (for send operations) + return 5 + 4 + (send_padd_blocksize - 1) + send_mac_buffer.length; + } + + public void sendMessage(byte[] message, int off, int len, int padd) throws IOException + { + if (padd < 4) + padd = 4; + else if (padd > 64) + padd = 64; + + if (send_comp != null && can_send_compress) { + if (send_comp_buffer.length < message.length + 1024) + send_comp_buffer = new byte[message.length + 1024]; + len = send_comp.compress(message, off, len, send_comp_buffer); + message = send_comp_buffer; + } + + int packet_len = 5 + len + padd; /* Minimum allowed padding is 4 */ + + int slack = packet_len % send_padd_blocksize; + + if (slack != 0) + { + packet_len += (send_padd_blocksize - slack); + } + + if (packet_len < 16) + packet_len = 16; + + int padd_len = packet_len - (5 + len); + + if (useRandomPadding) + { + for (int i = 0; i < padd_len; i = i + 4) + { + /* + * don't waste calls to rnd.nextInt() (by using only 8bit of the + * output). just believe me: even though we may write here up to 3 + * bytes which won't be used, there is no "buffer overflow" (i.e., + * arrayindexoutofbounds). the padding buffer is big enough =) (256 + * bytes, and that is bigger than any current cipher block size + 64). + */ + + int r = rnd.nextInt(); + send_padding_buffer[i] = (byte) r; + send_padding_buffer[i + 1] = (byte) (r >> 8); + send_padding_buffer[i + 2] = (byte) (r >> 16); + send_padding_buffer[i + 3] = (byte) (r >> 24); + } + } + else + { + /* use zero padding for unencrypted traffic */ + for (int i = 0; i < padd_len; i++) + send_padding_buffer[i] = 0; + /* Actually this code is paranoid: we never filled any + * bytes into the padding buffer so far, therefore it should + * consist of zeros only. + */ + } + + send_packet_header_buffer[0] = (byte) ((packet_len - 4) >> 24); + send_packet_header_buffer[1] = (byte) ((packet_len - 4) >> 16); + send_packet_header_buffer[2] = (byte) ((packet_len - 4) >> 8); + send_packet_header_buffer[3] = (byte) ((packet_len - 4)); + send_packet_header_buffer[4] = (byte) padd_len; + + cos.write(send_packet_header_buffer, 0, 5); + cos.write(message, off, len); + cos.write(send_padding_buffer, 0, padd_len); + + if (send_mac != null) + { + send_mac.initMac(send_seq_number); + send_mac.update(send_packet_header_buffer, 0, 5); + send_mac.update(message, off, len); + send_mac.update(send_padding_buffer, 0, padd_len); + + send_mac.getMac(send_mac_buffer, 0); + cos.writePlain(send_mac_buffer, 0, send_mac_buffer.length); + } + + cos.flush(); + + if (log.isEnabled()) + { + log.log(90, "Sent " + Packets.getMessageName(message[off] & 0xff) + " " + len + " bytes payload"); + } + + send_seq_number++; + } + + public int peekNextMessageLength() throws IOException + { + if (recv_packet_header_present == false) + { + cis.read(recv_packet_header_buffer, 0, 5); + recv_packet_header_present = true; + } + + int packet_length = ((recv_packet_header_buffer[0] & 0xff) << 24) + | ((recv_packet_header_buffer[1] & 0xff) << 16) | ((recv_packet_header_buffer[2] & 0xff) << 8) + | ((recv_packet_header_buffer[3] & 0xff)); + + int padding_length = recv_packet_header_buffer[4] & 0xff; + + if (packet_length > 35000 || packet_length < 12) + throw new IOException("Illegal packet size! (" + packet_length + ")"); + + int payload_length = packet_length - padding_length - 1; + + if (payload_length < 0) + throw new IOException("Illegal padding_length in packet from remote (" + padding_length + ")"); + + return payload_length; + } + + public int receiveMessage(byte buffer[], int off, int len) throws IOException + { + if (recv_packet_header_present == false) + { + cis.read(recv_packet_header_buffer, 0, 5); + } + else + recv_packet_header_present = false; + + int packet_length = ((recv_packet_header_buffer[0] & 0xff) << 24) + | ((recv_packet_header_buffer[1] & 0xff) << 16) | ((recv_packet_header_buffer[2] & 0xff) << 8) + | ((recv_packet_header_buffer[3] & 0xff)); + + int padding_length = recv_packet_header_buffer[4] & 0xff; + + if (packet_length > 35000 || packet_length < 12) + throw new IOException("Illegal packet size! (" + packet_length + ")"); + + int payload_length = packet_length - padding_length - 1; + + if (payload_length < 0) + throw new IOException("Illegal padding_length in packet from remote (" + padding_length + ")"); + + if (payload_length >= len) + throw new IOException("Receive buffer too small (" + len + ", need " + payload_length + ")"); + + cis.read(buffer, off, payload_length); + cis.read(recv_padding_buffer, 0, padding_length); + + if (recv_mac != null) + { + cis.readPlain(recv_mac_buffer, 0, recv_mac_buffer.length); + + recv_mac.initMac(recv_seq_number); + recv_mac.update(recv_packet_header_buffer, 0, 5); + recv_mac.update(buffer, off, payload_length); + recv_mac.update(recv_padding_buffer, 0, padding_length); + recv_mac.getMac(recv_mac_buffer_cmp, 0); + + for (int i = 0; i < recv_mac_buffer.length; i++) + { + if (recv_mac_buffer[i] != recv_mac_buffer_cmp[i]) + throw new IOException("Remote sent corrupt MAC."); + } + } + + recv_seq_number++; + + if (log.isEnabled()) + { + log.log(90, "Received " + Packets.getMessageName(buffer[off] & 0xff) + " " + payload_length + + " bytes payload"); + } + + if (recv_comp != null && can_recv_compress) { + int[] uncomp_len = new int[] { payload_length }; + buffer = recv_comp.uncompress(buffer, off, uncomp_len); + + if (buffer == null) { + throw new IOException("Error while inflating remote data"); + } else { + return uncomp_len[0]; + } + } else { + return payload_length; + } + } + + /** + * + */ + public void startCompression() { + can_recv_compress = true; + can_send_compress = true; + } +} diff --git a/app/src/main/java/com/trilead/ssh2/transport/TransportManager.java b/app/src/main/java/com/trilead/ssh2/transport/TransportManager.java new file mode 100644 index 0000000..2e88126 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/transport/TransportManager.java @@ -0,0 +1,802 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.Vector; + +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.HTTPProxyData; +import com.trilead.ssh2.HTTPProxyException; +import com.trilead.ssh2.ProxyData; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketDisconnect; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.util.Tokenizer; + + +/* + * Yes, the "standard" is a big mess. On one side, the say that arbitary channel + * packets are allowed during kex exchange, on the other side we need to blindly + * ignore the next _packet_ if the KEX guess was wrong. Where do we know from that + * the next packet is not a channel data packet? Yes, we could check if it is in + * the KEX range. But the standard says nothing about this. The OpenSSH guys + * block local "normal" traffic during KEX. That's fine - however, they assume + * that the other side is doing the same. During re-key, if they receive traffic + * other than KEX, they become horribly irritated and kill the connection. Since + * we are very likely going to communicate with OpenSSH servers, we have to play + * the same game - even though we could do better. + * + * btw: having stdout and stderr on the same channel, with a shared window, is + * also a VERY good idea... =( + */ + +/** + * TransportManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TransportManager.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TransportManager +{ + private static final Logger log = Logger.getLogger(TransportManager.class); + + class HandlerEntry + { + MessageHandler mh; + int low; + int high; + } + + private final Vector asynchronousQueue = new Vector(); + private Thread asynchronousThread = null; + + class AsynchronousWorker extends Thread + { + public void run() + { + while (true) + { + byte[] msg = null; + + synchronized (asynchronousQueue) + { + if (asynchronousQueue.size() == 0) + { + /* After the queue is empty for about 2 seconds, stop this thread */ + + try + { + asynchronousQueue.wait(2000); + } + catch (InterruptedException e) + { + /* OKOK, if somebody interrupts us, then we may die earlier. */ + } + + if (asynchronousQueue.size() == 0) + { + asynchronousThread = null; + return; + } + } + + msg = asynchronousQueue.remove(0); + } + + /* The following invocation may throw an IOException. + * There is no point in handling it - it simply means + * that the connection has a problem and we should stop + * sending asynchronously messages. We do not need to signal that + * we have exited (asynchronousThread = null): further + * messages in the queue cannot be sent by this or any + * other thread. + * Other threads will sooner or later (when receiving or + * sending the next message) get the same IOException and + * get to the same conclusion. + */ + + try + { + sendMessage(msg); + } + catch (IOException e) + { + return; + } + } + } + } + + String hostname; + int port; + final Socket sock = new Socket(); + + Object connectionSemaphore = new Object(); + + boolean flagKexOngoing = false; + boolean connectionClosed = false; + + Throwable reasonClosedCause = null; + + TransportConnection tc; + KexManager km; + + Vector messageHandlers = new Vector(); + + Thread receiveThread; + + Vector connectionMonitors = new Vector(); + boolean monitorsWereInformed = false; + + /** + * There were reports that there are JDKs which use + * the resolver even though one supplies a dotted IP + * address in the Socket constructor. That is why we + * try to generate the InetAdress "by hand". + * + * @param host + * @return the InetAddress + * @throws UnknownHostException + */ + private InetAddress createInetAddress(String host) throws UnknownHostException + { + /* Check if it is a dotted IP4 address */ + + InetAddress addr = parseIPv4Address(host); + + if (addr != null) + return addr; + + return InetAddress.getByName(host); + } + + private InetAddress parseIPv4Address(String host) throws UnknownHostException + { + if (host == null) + return null; + + String[] quad = Tokenizer.parseTokens(host, '.'); + + if ((quad == null) || (quad.length != 4)) + return null; + + byte[] addr = new byte[4]; + + for (int i = 0; i < 4; i++) + { + int part = 0; + + if ((quad[i].length() == 0) || (quad[i].length() > 3)) + return null; + + for (int k = 0; k < quad[i].length(); k++) + { + char c = quad[i].charAt(k); + + /* No, Character.isDigit is not the same */ + if ((c < '0') || (c > '9')) + return null; + + part = part * 10 + (c - '0'); + } + + if (part > 255) /* 300.1.2.3 is invalid =) */ + return null; + + addr[i] = (byte) part; + } + + return InetAddress.getByAddress(host, addr); + } + + public TransportManager(String host, int port) throws IOException + { + this.hostname = host; + this.port = port; + } + + public int getPacketOverheadEstimate() + { + return tc.getPacketOverheadEstimate(); + } + + public void setTcpNoDelay(boolean state) throws IOException + { + sock.setTcpNoDelay(state); + } + + public void setSoTimeout(int timeout) throws IOException + { + sock.setSoTimeout(timeout); + } + + public ConnectionInfo getConnectionInfo(int kexNumber) throws IOException + { + return km.getOrWaitForConnectionInfo(kexNumber); + } + + public Throwable getReasonClosedCause() + { + synchronized (connectionSemaphore) + { + return reasonClosedCause; + } + } + + public byte[] getSessionIdentifier() + { + return km.sessionId; + } + + public void close(Throwable cause, boolean useDisconnectPacket) + { + if (useDisconnectPacket == false) + { + /* OK, hard shutdown - do not aquire the semaphore, + * perhaps somebody is inside (and waits until the remote + * side is ready to accept new data). */ + + try + { + sock.close(); + } + catch (IOException ignore) + { + } + + /* OK, whoever tried to send data, should now agree that + * there is no point in further waiting =) + * It is safe now to aquire the semaphore. + */ + } + + synchronized (connectionSemaphore) + { + if (connectionClosed == false) + { + if (useDisconnectPacket == true) + { + try + { + byte[] msg = new PacketDisconnect(Packets.SSH_DISCONNECT_BY_APPLICATION, cause.getMessage(), "") + .getPayload(); + if (tc != null) + tc.sendMessage(msg); + } + catch (IOException ignore) + { + } + + try + { + sock.close(); + } + catch (IOException ignore) + { + } + } + + connectionClosed = true; + reasonClosedCause = cause; /* may be null */ + } + connectionSemaphore.notifyAll(); + } + + /* No check if we need to inform the monitors */ + + Vector monitors = null; + + synchronized (this) + { + /* Short term lock to protect "connectionMonitors" + * and "monitorsWereInformed" + * (they may be modified concurrently) + */ + + if (monitorsWereInformed == false) + { + monitorsWereInformed = true; + monitors = (Vector) connectionMonitors.clone(); + } + } + + if (monitors != null) + { + for (int i = 0; i < monitors.size(); i++) + { + try + { + ConnectionMonitor cmon = (ConnectionMonitor) monitors.elementAt(i); + cmon.connectionLost(reasonClosedCause); + } + catch (Exception ignore) + { + } + } + } + } + + private void establishConnection(ProxyData proxyData, int connectTimeout) throws IOException + { + /* See the comment for createInetAddress() */ + + if (proxyData == null) + { + InetAddress addr = createInetAddress(hostname); + sock.connect(new InetSocketAddress(addr, port), connectTimeout); + sock.setSoTimeout(0); + return; + } + + if (proxyData instanceof HTTPProxyData) + { + HTTPProxyData pd = (HTTPProxyData) proxyData; + + /* At the moment, we only support HTTP proxies */ + + InetAddress addr = createInetAddress(pd.proxyHost); + sock.connect(new InetSocketAddress(addr, pd.proxyPort), connectTimeout); + sock.setSoTimeout(0); + + /* OK, now tell the proxy where we actually want to connect to */ + + StringBuffer sb = new StringBuffer(); + + sb.append("CONNECT "); + sb.append(hostname); + sb.append(':'); + sb.append(port); + sb.append(" HTTP/1.0\r\n"); + + if ((pd.proxyUser != null) && (pd.proxyPass != null)) + { + String credentials = pd.proxyUser + ":" + pd.proxyPass; + char[] encoded = Base64.encode(credentials.getBytes("ISO-8859-1")); + sb.append("Proxy-Authorization: Basic "); + sb.append(encoded); + sb.append("\r\n"); + } + + if (pd.requestHeaderLines != null) + { + for (int i = 0; i < pd.requestHeaderLines.length; i++) + { + if (pd.requestHeaderLines[i] != null) + { + sb.append(pd.requestHeaderLines[i]); + sb.append("\r\n"); + } + } + } + + sb.append("\r\n"); + + OutputStream out = sock.getOutputStream(); + + out.write(sb.toString().getBytes("ISO-8859-1")); + out.flush(); + + /* Now parse the HTTP response */ + + byte[] buffer = new byte[1024]; + InputStream in = sock.getInputStream(); + + int len = ClientServerHello.readLineRN(in, buffer); + + String httpReponse = new String(buffer, 0, len, "ISO-8859-1"); + + if (httpReponse.startsWith("HTTP/") == false) + throw new IOException("The proxy did not send back a valid HTTP response."); + + /* "HTTP/1.X XYZ X" => 14 characters minimum */ + + if ((httpReponse.length() < 14) || (httpReponse.charAt(8) != ' ') || (httpReponse.charAt(12) != ' ')) + throw new IOException("The proxy did not send back a valid HTTP response."); + + int errorCode = 0; + + try + { + errorCode = Integer.parseInt(httpReponse.substring(9, 12)); + } + catch (NumberFormatException ignore) + { + throw new IOException("The proxy did not send back a valid HTTP response."); + } + + if ((errorCode < 0) || (errorCode > 999)) + throw new IOException("The proxy did not send back a valid HTTP response."); + + if (errorCode != 200) + { + throw new HTTPProxyException(httpReponse.substring(13), errorCode); + } + + /* OK, read until empty line */ + + while (true) + { + len = ClientServerHello.readLineRN(in, buffer); + if (len == 0) + break; + } + return; + } + + throw new IOException("Unsupported ProxyData"); + } + + public void initialize(CryptoWishList cwl, ServerHostKeyVerifier verifier, DHGexParameters dhgex, + int connectTimeout, SecureRandom rnd, ProxyData proxyData) throws IOException + { + /* First, establish the TCP connection to the SSH-2 server */ + + establishConnection(proxyData, connectTimeout); + + /* Parse the server line and say hello - important: this information is later needed for the + * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object + * for later use. + */ + + ClientServerHello csh = new ClientServerHello(sock.getInputStream(), sock.getOutputStream()); + + tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd); + + km = new KexManager(this, csh, cwl, hostname, port, verifier, rnd); + km.initiateKEX(cwl, dhgex); + + receiveThread = new Thread(new Runnable() + { + public void run() + { + try + { + receiveLoop(); + } + catch (IOException e) + { + close(e, false); + + if (log.isEnabled()) + log.log(10, "Receive thread: error in receiveLoop: " + e.getMessage()); + } + + if (log.isEnabled()) + log.log(50, "Receive thread: back from receiveLoop"); + + /* Tell all handlers that it is time to say goodbye */ + + if (km != null) + { + try + { + km.handleMessage(null, 0); + } + catch (IOException e) + { + } + } + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + try + { + he.mh.handleMessage(null, 0); + } + catch (Exception ignore) + { + } + } + } + }); + + receiveThread.setDaemon(true); + receiveThread.start(); + } + + public void registerMessageHandler(MessageHandler mh, int low, int high) + { + HandlerEntry he = new HandlerEntry(); + he.mh = mh; + he.low = low; + he.high = high; + + synchronized (messageHandlers) + { + messageHandlers.addElement(he); + } + } + + public void removeMessageHandler(MessageHandler mh, int low, int high) + { + synchronized (messageHandlers) + { + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + if ((he.mh == mh) && (he.low == low) && (he.high == high)) + { + messageHandlers.removeElementAt(i); + break; + } + } + } + } + + public void sendKexMessage(byte[] msg) throws IOException + { + synchronized (connectionSemaphore) + { + if (connectionClosed) + { + throw (IOException) new IOException("Sorry, this connection is closed.").initCause(reasonClosedCause); + } + + flagKexOngoing = true; + + try + { + tc.sendMessage(msg); + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + + public void kexFinished() throws IOException + { + synchronized (connectionSemaphore) + { + flagKexOngoing = false; + connectionSemaphore.notifyAll(); + } + } + + public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex) throws IOException + { + km.initiateKEX(cwl, dhgex); + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + tc.changeRecvCipher(bc, mac); + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + tc.changeSendCipher(bc, mac); + } + + /** + * @param comp + */ + public void changeRecvCompression(ICompressor comp) { + tc.changeRecvCompression(comp); + } + + /** + * @param comp + */ + public void changeSendCompression(ICompressor comp) { + tc.changeSendCompression(comp); + } + + /** + * + */ + public void startCompression() { + tc.startCompression(); + } + + public void sendAsynchronousMessage(byte[] msg) throws IOException + { + synchronized (asynchronousQueue) + { + asynchronousQueue.addElement(msg); + + /* This limit should be flexible enough. We need this, otherwise the peer + * can flood us with global requests (and other stuff where we have to reply + * with an asynchronous message) and (if the server just sends data and does not + * read what we send) this will probably put us in a low memory situation + * (our send queue would grow and grow and...) */ + + if (asynchronousQueue.size() > 100) + throw new IOException("Error: the peer is not consuming our asynchronous replies."); + + /* Check if we have an asynchronous sending thread */ + + if (asynchronousThread == null) + { + asynchronousThread = new AsynchronousWorker(); + asynchronousThread.setDaemon(true); + asynchronousThread.start(); + + /* The thread will stop after 2 seconds of inactivity (i.e., empty queue) */ + } + } + } + + public void setConnectionMonitors(Vector monitors) + { + synchronized (this) + { + connectionMonitors = (Vector) monitors.clone(); + } + } + + public void sendMessage(byte[] msg) throws IOException + { + if (Thread.currentThread() == receiveThread) + throw new IOException("Assertion error: sendMessage may never be invoked by the receiver thread!"); + + synchronized (connectionSemaphore) + { + while (true) + { + if (connectionClosed) + { + throw (IOException) new IOException("Sorry, this connection is closed.") + .initCause(reasonClosedCause); + } + + if (flagKexOngoing == false) + break; + + try + { + connectionSemaphore.wait(); + } + catch (InterruptedException e) + { + } + } + + try + { + tc.sendMessage(msg); + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + + public void receiveLoop() throws IOException + { + byte[] msg = new byte[35000]; + + while (true) + { + int msglen = tc.receiveMessage(msg, 0, msg.length); + + int type = msg[0] & 0xff; + + if (type == Packets.SSH_MSG_IGNORE) + continue; + + if (type == Packets.SSH_MSG_DEBUG) + { + if (log.isEnabled()) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + tr.readBoolean(); + StringBuffer debugMessageBuffer = new StringBuffer(); + debugMessageBuffer.append(tr.readString("UTF-8")); + + for (int i = 0; i < debugMessageBuffer.length(); i++) + { + char c = debugMessageBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + debugMessageBuffer.setCharAt(i, '\uFFFD'); + } + + log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'"); + } + continue; + } + + if (type == Packets.SSH_MSG_UNIMPLEMENTED) + { + throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen."); + } + + if (type == Packets.SSH_MSG_DISCONNECT) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + int reason_code = tr.readUINT32(); + StringBuffer reasonBuffer = new StringBuffer(); + reasonBuffer.append(tr.readString("UTF-8")); + + /* + * Do not get fooled by servers that send abnormal long error + * messages + */ + + if (reasonBuffer.length() > 255) + { + reasonBuffer.setLength(255); + reasonBuffer.setCharAt(254, '.'); + reasonBuffer.setCharAt(253, '.'); + reasonBuffer.setCharAt(252, '.'); + } + + /* + * Also, check that the server did not send charcaters that may + * screw up the receiver -> restrict to reasonable US-ASCII + * subset -> "printable characters" (ASCII 32 - 126). Replace + * all others with 0xFFFD (UNICODE replacement character). + */ + + for (int i = 0; i < reasonBuffer.length(); i++) + { + char c = reasonBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + reasonBuffer.setCharAt(i, '\uFFFD'); + } + + throw new IOException("Peer sent DISCONNECT message (reason code " + reason_code + "): " + + reasonBuffer.toString()); + } + + /* + * Is it a KEX Packet? + */ + + if ((type == Packets.SSH_MSG_KEXINIT) || (type == Packets.SSH_MSG_NEWKEYS) + || ((type >= 30) && (type <= 49))) + { + km.handleMessage(msg, msglen); + continue; + } + + if (type == Packets.SSH_MSG_USERAUTH_SUCCESS) { + tc.startCompression(); + } + + MessageHandler mh = null; + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + if ((he.low <= type) && (type <= he.high)) + { + mh = he.mh; + break; + } + } + + if (mh == null) + throw new IOException("Unexpected SSH message (type " + type + ")"); + + mh.handleMessage(msg, msglen); + } + } +} diff --git a/app/src/main/java/com/trilead/ssh2/util/TimeoutService.java b/app/src/main/java/com/trilead/ssh2/util/TimeoutService.java new file mode 100644 index 0000000..3d52161 --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/util/TimeoutService.java @@ -0,0 +1,149 @@ + +package com.trilead.ssh2.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collections; +import java.util.LinkedList; + +import com.trilead.ssh2.log.Logger; + + +/** + * TimeoutService (beta). Here you can register a timeout. + *

+ * Implemented having large scale programs in mind: if you open many concurrent SSH connections + * that rely on timeouts, then there will be only one timeout thread. Once all timeouts + * have expired/are cancelled, the thread will (sooner or later) exit. + * Only after new timeouts arrive a new thread (singleton) will be instantiated. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TimeoutService.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class TimeoutService +{ + private static final Logger log = Logger.getLogger(TimeoutService.class); + + public static class TimeoutToken implements Comparable + { + private long runTime; + private Runnable handler; + + private TimeoutToken(long runTime, Runnable handler) + { + this.runTime = runTime; + this.handler = handler; + } + + public int compareTo(Object o) + { + TimeoutToken t = (TimeoutToken) o; + if (runTime > t.runTime) + return 1; + if (runTime == t.runTime) + return 0; + return -1; + } + } + + private static class TimeoutThread extends Thread + { + public void run() + { + synchronized (todolist) + { + while (true) + { + if (todolist.size() == 0) + { + timeoutThread = null; + return; + } + + long now = System.currentTimeMillis(); + + TimeoutToken tt = (TimeoutToken) todolist.getFirst(); + + if (tt.runTime > now) + { + /* Not ready yet, sleep a little bit */ + + try + { + todolist.wait(tt.runTime - now); + } + catch (InterruptedException e) + { + } + + /* We cannot simply go on, since it could be that the token + * was removed (cancelled) or another one has been inserted in + * the meantime. + */ + + continue; + } + + todolist.removeFirst(); + + try + { + tt.handler.run(); + } + catch (Exception e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + log.log(20, "Exeception in Timeout handler:" + e.getMessage() + "(" + sw.toString() + ")"); + } + } + } + } + } + + /* The list object is also used for locking purposes */ + private static final LinkedList todolist = new LinkedList(); + + private static Thread timeoutThread = null; + + /** + * It is assumed that the passed handler will not execute for a long time. + * + * @param runTime + * @param handler + * @return a TimeoutToken that can be used to cancel the timeout. + */ + public static final TimeoutToken addTimeoutHandler(long runTime, Runnable handler) + { + TimeoutToken token = new TimeoutToken(runTime, handler); + + synchronized (todolist) + { + todolist.add(token); + Collections.sort(todolist); + + if (timeoutThread != null) + timeoutThread.interrupt(); + else + { + timeoutThread = new TimeoutThread(); + timeoutThread.setDaemon(true); + timeoutThread.start(); + } + } + + return token; + } + + public static final void cancelTimeoutHandler(TimeoutToken token) + { + synchronized (todolist) + { + todolist.remove(token); + + if (timeoutThread != null) + timeoutThread.interrupt(); + } + } + +} diff --git a/app/src/main/java/com/trilead/ssh2/util/Tokenizer.java b/app/src/main/java/com/trilead/ssh2/util/Tokenizer.java new file mode 100644 index 0000000..dfd480b --- /dev/null +++ b/app/src/main/java/com/trilead/ssh2/util/Tokenizer.java @@ -0,0 +1,51 @@ + +package com.trilead.ssh2.util; + +/** + * Tokenizer. Why? Because StringTokenizer is not available in J2ME. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: Tokenizer.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class Tokenizer +{ + /** + * Exists because StringTokenizer is not available in J2ME. + * Returns an array with at least 1 entry. + * + * @param source must be non-null + * @param delimiter + * @return an array of Strings + */ + public static String[] parseTokens(String source, char delimiter) + { + int numtoken = 1; + + for (int i = 0; i < source.length(); i++) + { + if (source.charAt(i) == delimiter) + numtoken++; + } + + String list[] = new String[numtoken]; + int nextfield = 0; + + for (int i = 0; i < numtoken; i++) + { + if (nextfield >= source.length()) + { + list[i] = ""; + } + else + { + int idx = source.indexOf(delimiter, nextfield); + if (idx == -1) + idx = source.length(); + list[i] = source.substring(nextfield, idx); + nextfield = idx + 1; + } + } + + return list; + } +} diff --git a/app/src/main/java/de/mud/telnet/TelnetProtocolHandler.java b/app/src/main/java/de/mud/telnet/TelnetProtocolHandler.java new file mode 100644 index 0000000..74f08bb --- /dev/null +++ b/app/src/main/java/de/mud/telnet/TelnetProtocolHandler.java @@ -0,0 +1,678 @@ +/* + * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". + * + * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved. + * + * Please visit http://javatelnet.org/ for updates and contact. + * + * --LICENSE NOTICE-- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + * --LICENSE NOTICE-- + * + */ + +package de.mud.telnet; + +import java.io.IOException; +/** + * This is a telnet protocol handler. The handler needs implementations + * for several methods to handle the telnet options and to be able to + * read and write the buffer. + *

+ * Maintainer: Marcus Meissner + * + * @version $Id: TelnetProtocolHandler.java 503 2005-10-24 07:34:13Z marcus $ + * @author Matthias L. Jugel, Marcus Meissner + */ +public abstract class TelnetProtocolHandler { + /** contains the current revision id */ + public final static String ID = "$Id: TelnetProtocolHandler.java 503 2005-10-24 07:34:13Z marcus $"; + + /** debug level */ + private final static int debug = 0; + + /** temporary buffer for data-telnetstuff-data transformation */ + private byte[] tempbuf = new byte[0]; + + /** the data sent on pressing \n */ + private byte[] crlf = new byte[2]; + /** the data sent on pressing \r */ + private byte[] cr = new byte[2]; + + /** + * Create a new telnet protocol handler. + */ + public TelnetProtocolHandler() { + reset(); + + crlf[0] = 13; crlf[1] = 10; + cr[0] = 13; cr[1] = 0; + } + + /** + * Get the current terminal type for TTYPE telnet option. + * @return the string id of the terminal + */ + protected abstract String getTerminalType(); + + /** + * Get the current window size of the terminal for the + * NAWS telnet option. + * @return the size of the terminal as Dimension + */ + protected abstract int[] getWindowSize(); + + /** + * Set the local echo option of telnet. + * @param echo true for local echo, false for no local echo + */ + protected abstract void setLocalEcho(boolean echo); + + /** + * Generate an EOR (end of record) request. For use by prompt displaying. + */ + protected abstract void notifyEndOfRecord(); + + /** + * Send data to the remote host. + * @param b array of bytes to send + */ + protected abstract void write(byte[] b) throws IOException; + + /** + * Read the charset name from terminal. + */ + protected abstract String getCharsetName(); + + /** + * Send one byte to the remote host. + * @param b the byte to be sent + * @see #write(byte[] b) + */ + private static byte[] one = new byte[1]; + private void write(byte b) throws IOException { + one[0] = b; + write(one); + } + + /** + * Reset the protocol handler. This may be necessary after the + * connection was closed or some other problem occured. + */ + public void reset() { + neg_state = 0; + receivedDX = new byte[256]; + sentDX = new byte[256]; + receivedWX = new byte[256]; + sentWX = new byte[256]; + } + + // =================================================================== + // the actual negotiation handling for the telnet protocol follows: + // =================================================================== + + /** state variable for telnet negotiation reader */ + private byte neg_state = 0; + + /** constants for the negotiation state */ + private final static byte STATE_DATA = 0; + private final static byte STATE_IAC = 1; + private final static byte STATE_IACSB = 2; + private final static byte STATE_IACWILL = 3; + private final static byte STATE_IACDO = 4; + private final static byte STATE_IACWONT = 5; + private final static byte STATE_IACDONT = 6; + private final static byte STATE_IACSBIAC = 7; + private final static byte STATE_IACSBDATA = 8; + private final static byte STATE_IACSBDATAIAC = 9; + + /** What IAC SB we are handling right now */ + private byte current_sb; + + /** current SB negotiation buffer */ + private byte[] sbbuf; + + /** IAC - init sequence for telnet negotiation. */ + private final static byte IAC = (byte)255; + /** [IAC] End Of Record */ + private final static byte EOR = (byte)239; + /** [IAC] WILL */ + private final static byte WILL = (byte)251; + /** [IAC] WONT */ + private final static byte WONT = (byte)252; + /** [IAC] DO */ + private final static byte DO = (byte)253; + /** [IAC] DONT */ + private final static byte DONT = (byte)254; + /** [IAC] Sub Begin */ + private final static byte SB = (byte)250; + /** [IAC] Sub End */ + private final static byte SE = (byte)240; + /** Telnet option: binary mode */ + private final static byte TELOPT_BINARY= (byte)0; /* binary mode */ + /** Telnet option: echo text */ + private final static byte TELOPT_ECHO = (byte)1; /* echo on/off */ + /** Telnet option: sga */ + private final static byte TELOPT_SGA = (byte)3; /* supress go ahead */ + /** Telnet option: End Of Record */ + private final static byte TELOPT_EOR = (byte)25; /* end of record */ + /** Telnet option: Negotiate About Window Size */ + private final static byte TELOPT_NAWS = (byte)31; /* NA-WindowSize*/ + /** Telnet option: Terminal Type */ + private final static byte TELOPT_TTYPE = (byte)24; /* terminal type */ + /** Telnet option: CHARSET */ + private final static byte TELOPT_CHARSET= (byte)42; /* charset */ + + private final static byte[] IACWILL = { IAC, WILL }; + private final static byte[] IACWONT = { IAC, WONT }; + private final static byte[] IACDO = { IAC, DO }; + private final static byte[] IACDONT = { IAC, DONT }; + private final static byte[] IACSB = { IAC, SB }; + private final static byte[] IACSE = { IAC, SE }; + + private final static byte CHARSET_ACCEPTED = (byte)2; + private final static byte CHARSET_REJECTED = (byte)3; + + /** Telnet option qualifier 'IS' */ + private final static byte TELQUAL_IS = (byte)0; + /** Telnet option qualifier 'SEND' */ + private final static byte TELQUAL_SEND = (byte)1; + + /** What IAC DO(NT) request do we have received already ? */ + private byte[] receivedDX; + /** What IAC WILL/WONT request do we have received already ? */ + private byte[] receivedWX; + /** What IAC DO/DONT request do we have sent already ? */ + private byte[] sentDX; + /** What IAC WILL/WONT request do we have sent already ? */ + private byte[] sentWX; + + /** + * Send a Telnet Escape character (IAC ) + */ + public void sendTelnetControl(byte code) + throws IOException { + byte[] b = new byte[2]; + + b[0] = IAC; + b[1] = code; + write(b); + } + + /** + * Send the new Window Size (via NAWS) + */ + public void setWindowSize(int columns,int rows) + throws IOException { + if(debug > 2) System.err.println("sending NAWS"); + + if (receivedDX[TELOPT_NAWS] != DO) { + System.err.println("not allowed to send NAWS? (DONT NAWS)"); + return; + } + write(IAC);write(SB);write(TELOPT_NAWS); + write((byte) (columns >> 8)); + write((byte) (columns & 0xff)); + write((byte) (rows >> 8)); + write((byte) (rows & 0xff)); + write(IAC);write(SE); + } + + + /** + * Handle an incoming IAC SB <type> <bytes> IAC SE + * @param type type of SB + * @param sbata byte array as <bytes> + */ + private void handle_sb(byte type, byte[] sbdata) + throws IOException { + if(debug > 1) + System.err.println("TelnetIO.handle_sb("+type+")"); + switch (type) { + case TELOPT_TTYPE: + if (sbdata.length>0 && sbdata[0]==TELQUAL_SEND) { + write(IACSB);write(TELOPT_TTYPE);write(TELQUAL_IS); + /* FIXME: need more logic here if we use + * more than one terminal type + */ + String ttype = getTerminalType(); + if(ttype == null) ttype = "dumb"; + write(ttype.getBytes()); + write(IACSE); + } + break; + case TELOPT_CHARSET: + System.out.println("Got SB CHARSET"); + + String charsetStr = new String(sbdata, "US-ASCII"); + if (charsetStr.startsWith("TTABLE ")) { + charsetStr = charsetStr.substring(7); + } + String[] charsets = charsetStr.split(charsetStr.substring(0,0)); + String myCharset = getCharsetName(); + for (String charset : charsets) { + if (charset.equals(myCharset)) { + write(IACSB);write(TELOPT_CHARSET);write(CHARSET_ACCEPTED); + write(charset.getBytes()); + write(IACSE); + System.out.println("Sent our charset!"); + return; + } + } + write(IACSB);write(TELOPT_CHARSET);write(CHARSET_REJECTED); + write(IACSE); + break; + } + } + + /** + * Do not send any notifications at startup. We do not know, + * whether the remote client understands telnet protocol handling, + * so we are silent. + * (This used to send IAC WILL SGA, but this is false for a compliant + * client.) + */ + public void startup() throws IOException { + } + /** + * Transpose special telnet codes like 0xff or newlines to values + * that are compliant to the protocol. This method will also send + * the buffer immediately after transposing the data. + * @param buf the data buffer to be sent + */ + public void transpose(byte[] buf) throws IOException { + int i; + + byte[] nbuf,xbuf; + int nbufptr=0; + nbuf = new byte[buf.length*2]; // FIXME: buffer overflows possible + + for (i = 0; i < buf.length ; i++) { + switch (buf[i]) { + // Escape IAC twice in stream ... to be telnet protocol compliant + // this is there in binary and non-binary mode. + case IAC: + nbuf[nbufptr++]=IAC; + nbuf[nbufptr++]=IAC; + break; + // We need to heed RFC 854. LF (\n) is 10, CR (\r) is 13 + // we assume that the Terminal sends \n for lf+cr and \r for just cr + // linefeed+carriage return is CR LF */ + case 10: // \n + if (receivedDX[TELOPT_BINARY + 128 ] != DO) { + while (nbuf.length - nbufptr < crlf.length) { + xbuf = new byte[nbuf.length*2]; + System.arraycopy(nbuf,0,xbuf,0,nbufptr); + nbuf = xbuf; + } + for (int j=0;jRFC-Telnet + * @param nbuf the byte buffer put out after negotiation + * @return number of bytes processed, 0 for none, and -1 for end of buffer. + */ + public int negotiate(byte nbuf[], int offset) + throws IOException + { + int count = tempbuf.length; + byte[] buf = tempbuf; + byte sendbuf[] = new byte[3]; + byte b,reply; + int boffset = 0, noffset = offset; + boolean dobreak = false; + + if (count == 0) // buffer is empty. + return -1; + + while(!dobreak && (boffset < count) && (noffset < nbuf.length)) { + b=buf[boffset++]; + // of course, byte is a signed entity (-128 -> 127) + // but apparently the SGI Netscape 3.0 doesn't seem + // to care and provides happily values up to 255 + if (b>=128) + b=(byte)(b-256); + if(debug > 2) { + Byte B = new Byte(b); + System.err.print("byte: " + B.intValue()+ " "); + } + switch (neg_state) { + case STATE_DATA: + if (b==IAC) { + neg_state = STATE_IAC; + dobreak = true; // leave the loop so we can sync. + } else + nbuf[noffset++]=b; + break; + case STATE_IAC: + switch (b) { + case IAC: + if(debug > 2) System.err.print("IAC "); + neg_state = STATE_DATA; + nbuf[noffset++]=IAC; + break; + case WILL: + if(debug > 2) System.err.print("WILL "); + neg_state = STATE_IACWILL; + break; + case WONT: + if(debug > 2) System.err.print("WONT "); + neg_state = STATE_IACWONT; + break; + case DONT: + if(debug > 2) System.err.print("DONT "); + neg_state = STATE_IACDONT; + break; + case DO: + if(debug > 2) System.err.print("DO "); + neg_state = STATE_IACDO; + break; + case EOR: + if(debug > 1) System.err.print("EOR "); + notifyEndOfRecord(); + dobreak = true; // leave the loop so we can sync. + neg_state = STATE_DATA; + break; + case SB: + if(debug > 2) System.err.print("SB "); + neg_state = STATE_IACSB; + break; + default: + if(debug > 2) System.err.print(" "); + neg_state = STATE_DATA; + break; + } + break; + case STATE_IACWILL: + switch(b) { + case TELOPT_ECHO: + if(debug > 2) System.err.println("ECHO"); + reply = DO; + setLocalEcho(false); + break; + case TELOPT_SGA: + if(debug > 2) System.err.println("SGA"); + reply = DO; + break; + case TELOPT_EOR: + if(debug > 2) System.err.println("EOR"); + reply = DO; + break; + case TELOPT_BINARY: + if(debug > 2) System.err.println("BINARY"); + reply = DO; + break; + default: + if(debug > 2) System.err.println(""); + reply = DONT; + break; + } + if(debug > 1) System.err.println("<"+b+", WILL ="+WILL+">"); + if (reply != sentDX[b+128] || WILL != receivedWX[b+128]) { + sendbuf[0]=IAC; + sendbuf[1]=reply; + sendbuf[2]=b; + write(sendbuf); + sentDX[b+128] = reply; + receivedWX[b+128] = WILL; + } + neg_state = STATE_DATA; + break; + case STATE_IACWONT: + switch(b) { + case TELOPT_ECHO: + if(debug > 2) System.err.println("ECHO"); + setLocalEcho(true); + reply = DONT; + break; + case TELOPT_SGA: + if(debug > 2) System.err.println("SGA"); + reply = DONT; + break; + case TELOPT_EOR: + if(debug > 2) System.err.println("EOR"); + reply = DONT; + break; + case TELOPT_BINARY: + if(debug > 2) System.err.println("BINARY"); + reply = DONT; + break; + default: + if(debug > 2) System.err.println(""); + reply = DONT; + break; + } + if(reply != sentDX[b+128] || WONT != receivedWX[b+128]) { + sendbuf[0]=IAC; + sendbuf[1]=reply; + sendbuf[2]=b; + write(sendbuf); + sentDX[b+128] = reply; + receivedWX[b+128] = WILL; + } + neg_state = STATE_DATA; + break; + case STATE_IACDO: + switch (b) { + case TELOPT_ECHO: + if(debug > 2) System.err.println("ECHO"); + reply = WILL; + setLocalEcho(true); + break; + case TELOPT_SGA: + if(debug > 2) System.err.println("SGA"); + reply = WILL; + break; + case TELOPT_TTYPE: + if(debug > 2) System.err.println("TTYPE"); + reply = WILL; + break; + case TELOPT_BINARY: + if(debug > 2) System.err.println("BINARY"); + reply = WILL; + break; + case TELOPT_NAWS: + if(debug > 2) System.err.println("NAWS"); + int[] size = getWindowSize(); + receivedDX[b] = DO; + if(size == null) { + // this shouldn't happen + write(IAC); + write(WONT); + write(TELOPT_NAWS); + reply = WONT; + sentWX[b] = WONT; + break; + } + reply = WILL; + sentWX[b] = WILL; + sendbuf[0]=IAC; + sendbuf[1]=WILL; + sendbuf[2]=TELOPT_NAWS; + write(sendbuf); + write(IAC);write(SB);write(TELOPT_NAWS); + write((byte) (size[0] >> 8)); + write((byte) (size[0] & 0xff)); + write((byte) (size[1] >> 8)); + write((byte) (size[1] & 0xff)); + write(IAC);write(SE); + break; + default: + if(debug > 2) System.err.println(""); + reply = WONT; + break; + } + if(reply != sentWX[128+b] || DO != receivedDX[128+b]) { + sendbuf[0]=IAC; + sendbuf[1]=reply; + sendbuf[2]=b; + write(sendbuf); + sentWX[b+128] = reply; + receivedDX[b+128] = DO; + } + neg_state = STATE_DATA; + break; + case STATE_IACDONT: + switch (b) { + case TELOPT_ECHO: + if(debug > 2) System.err.println("ECHO"); + reply = WONT; + setLocalEcho(false); + break; + case TELOPT_SGA: + if(debug > 2) System.err.println("SGA"); + reply = WONT; + break; + case TELOPT_NAWS: + if(debug > 2) System.err.println("NAWS"); + reply = WONT; + break; + case TELOPT_BINARY: + if(debug > 2) System.err.println("BINARY"); + reply = WONT; + break; + default: + if(debug > 2) System.err.println(""); + reply = WONT; + break; + } + if(reply != sentWX[b+128] || DONT != receivedDX[b+128]) { + write(IAC);write(reply);write(b); + sentWX[b+128] = reply; + receivedDX[b+128] = DONT; + } + neg_state = STATE_DATA; + break; + case STATE_IACSBIAC: + if(debug > 2) System.err.println(""+b+" "); + if (b == IAC) { + sbbuf = new byte[0]; + current_sb = b; + neg_state = STATE_IACSBDATA; + } else { + System.err.println("(bad) "+b+" "); + neg_state = STATE_DATA; + } + break; + case STATE_IACSB: + if(debug > 2) System.err.println(""+b+" "); + switch (b) { + case IAC: + neg_state = STATE_IACSBIAC; + break; + default: + current_sb = b; + sbbuf = new byte[0]; + neg_state = STATE_IACSBDATA; + break; + } + break; + case STATE_IACSBDATA: + if (debug > 2) System.err.println(""+b+" "); + switch (b) { + case IAC: + neg_state = STATE_IACSBDATAIAC; + break; + default: + byte[] xsb = new byte[sbbuf.length+1]; + System.arraycopy(sbbuf,0,xsb,0,sbbuf.length); + sbbuf = xsb; + sbbuf[sbbuf.length-1] = b; + break; + } + break; + case STATE_IACSBDATAIAC: + if (debug > 2) System.err.println(""+b+" "); + switch (b) { + case IAC: + neg_state = STATE_IACSBDATA; + byte[] xsb = new byte[sbbuf.length+1]; + System.arraycopy(sbbuf,0,xsb,0,sbbuf.length); + sbbuf = xsb; + sbbuf[sbbuf.length-1] = IAC; + break; + case SE: + handle_sb(current_sb,sbbuf); + current_sb = 0; + neg_state = STATE_DATA; + break; + case SB: + handle_sb(current_sb,sbbuf); + neg_state = STATE_IACSB; + break; + default: + neg_state = STATE_DATA; + break; + } + break; + default: + if (debug > 1) + System.err.println("This should not happen: "+neg_state+" "); + neg_state = STATE_DATA; + break; + } + } + // shrink tempbuf to new processed size. + byte[] xb = new byte[count-boffset]; + System.arraycopy(tempbuf,boffset,xb,0,count-boffset); + tempbuf = xb; + return noffset - offset; + } + + public void inputfeed(byte[] b, int offset, int len) { + byte[] xb = new byte[tempbuf.length+len]; + + System.arraycopy(tempbuf,0,xb,0,tempbuf.length); + System.arraycopy(b,offset,xb,tempbuf.length,len); + tempbuf = xb; + } +} diff --git a/app/src/main/java/de/mud/terminal/Precomposer.java b/app/src/main/java/de/mud/terminal/Precomposer.java new file mode 100644 index 0000000..edad64c --- /dev/null +++ b/app/src/main/java/de/mud/terminal/Precomposer.java @@ -0,0 +1,1052 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.mud.terminal; + +/** + * @author Kenny Root + * This data was taken from xterm's precompose.c + */ +public class Precomposer { + public final static char precompositions[][] = { + { 0x226E, 0x003C, 0x0338}, + { 0x2260, 0x003D, 0x0338}, + { 0x226F, 0x003E, 0x0338}, + { 0x00C0, 0x0041, 0x0300}, + { 0x00C1, 0x0041, 0x0301}, + { 0x00C2, 0x0041, 0x0302}, + { 0x00C3, 0x0041, 0x0303}, + { 0x0100, 0x0041, 0x0304}, + { 0x0102, 0x0041, 0x0306}, + { 0x0226, 0x0041, 0x0307}, + { 0x00C4, 0x0041, 0x0308}, + { 0x1EA2, 0x0041, 0x0309}, + { 0x00C5, 0x0041, 0x030A}, + { 0x01CD, 0x0041, 0x030C}, + { 0x0200, 0x0041, 0x030F}, + { 0x0202, 0x0041, 0x0311}, + { 0x1EA0, 0x0041, 0x0323}, + { 0x1E00, 0x0041, 0x0325}, + { 0x0104, 0x0041, 0x0328}, + { 0x1E02, 0x0042, 0x0307}, + { 0x1E04, 0x0042, 0x0323}, + { 0x1E06, 0x0042, 0x0331}, + { 0x0106, 0x0043, 0x0301}, + { 0x0108, 0x0043, 0x0302}, + { 0x010A, 0x0043, 0x0307}, + { 0x010C, 0x0043, 0x030C}, + { 0x00C7, 0x0043, 0x0327}, + { 0x1E0A, 0x0044, 0x0307}, + { 0x010E, 0x0044, 0x030C}, + { 0x1E0C, 0x0044, 0x0323}, + { 0x1E10, 0x0044, 0x0327}, + { 0x1E12, 0x0044, 0x032D}, + { 0x1E0E, 0x0044, 0x0331}, + { 0x00C8, 0x0045, 0x0300}, + { 0x00C9, 0x0045, 0x0301}, + { 0x00CA, 0x0045, 0x0302}, + { 0x1EBC, 0x0045, 0x0303}, + { 0x0112, 0x0045, 0x0304}, + { 0x0114, 0x0045, 0x0306}, + { 0x0116, 0x0045, 0x0307}, + { 0x00CB, 0x0045, 0x0308}, + { 0x1EBA, 0x0045, 0x0309}, + { 0x011A, 0x0045, 0x030C}, + { 0x0204, 0x0045, 0x030F}, + { 0x0206, 0x0045, 0x0311}, + { 0x1EB8, 0x0045, 0x0323}, + { 0x0228, 0x0045, 0x0327}, + { 0x0118, 0x0045, 0x0328}, + { 0x1E18, 0x0045, 0x032D}, + { 0x1E1A, 0x0045, 0x0330}, + { 0x1E1E, 0x0046, 0x0307}, + { 0x01F4, 0x0047, 0x0301}, + { 0x011C, 0x0047, 0x0302}, + { 0x1E20, 0x0047, 0x0304}, + { 0x011E, 0x0047, 0x0306}, + { 0x0120, 0x0047, 0x0307}, + { 0x01E6, 0x0047, 0x030C}, + { 0x0122, 0x0047, 0x0327}, + { 0x0124, 0x0048, 0x0302}, + { 0x1E22, 0x0048, 0x0307}, + { 0x1E26, 0x0048, 0x0308}, + { 0x021E, 0x0048, 0x030C}, + { 0x1E24, 0x0048, 0x0323}, + { 0x1E28, 0x0048, 0x0327}, + { 0x1E2A, 0x0048, 0x032E}, + { 0x00CC, 0x0049, 0x0300}, + { 0x00CD, 0x0049, 0x0301}, + { 0x00CE, 0x0049, 0x0302}, + { 0x0128, 0x0049, 0x0303}, + { 0x012A, 0x0049, 0x0304}, + { 0x012C, 0x0049, 0x0306}, + { 0x0130, 0x0049, 0x0307}, + { 0x00CF, 0x0049, 0x0308}, + { 0x1EC8, 0x0049, 0x0309}, + { 0x01CF, 0x0049, 0x030C}, + { 0x0208, 0x0049, 0x030F}, + { 0x020A, 0x0049, 0x0311}, + { 0x1ECA, 0x0049, 0x0323}, + { 0x012E, 0x0049, 0x0328}, + { 0x1E2C, 0x0049, 0x0330}, + { 0x0134, 0x004A, 0x0302}, + { 0x1E30, 0x004B, 0x0301}, + { 0x01E8, 0x004B, 0x030C}, + { 0x1E32, 0x004B, 0x0323}, + { 0x0136, 0x004B, 0x0327}, + { 0x1E34, 0x004B, 0x0331}, + { 0x0139, 0x004C, 0x0301}, + { 0x013D, 0x004C, 0x030C}, + { 0x1E36, 0x004C, 0x0323}, + { 0x013B, 0x004C, 0x0327}, + { 0x1E3C, 0x004C, 0x032D}, + { 0x1E3A, 0x004C, 0x0331}, + { 0x1E3E, 0x004D, 0x0301}, + { 0x1E40, 0x004D, 0x0307}, + { 0x1E42, 0x004D, 0x0323}, + { 0x01F8, 0x004E, 0x0300}, + { 0x0143, 0x004E, 0x0301}, + { 0x00D1, 0x004E, 0x0303}, + { 0x1E44, 0x004E, 0x0307}, + { 0x0147, 0x004E, 0x030C}, + { 0x1E46, 0x004E, 0x0323}, + { 0x0145, 0x004E, 0x0327}, + { 0x1E4A, 0x004E, 0x032D}, + { 0x1E48, 0x004E, 0x0331}, + { 0x00D2, 0x004F, 0x0300}, + { 0x00D3, 0x004F, 0x0301}, + { 0x00D4, 0x004F, 0x0302}, + { 0x00D5, 0x004F, 0x0303}, + { 0x014C, 0x004F, 0x0304}, + { 0x014E, 0x004F, 0x0306}, + { 0x022E, 0x004F, 0x0307}, + { 0x00D6, 0x004F, 0x0308}, + { 0x1ECE, 0x004F, 0x0309}, + { 0x0150, 0x004F, 0x030B}, + { 0x01D1, 0x004F, 0x030C}, + { 0x020C, 0x004F, 0x030F}, + { 0x020E, 0x004F, 0x0311}, + { 0x01A0, 0x004F, 0x031B}, + { 0x1ECC, 0x004F, 0x0323}, + { 0x01EA, 0x004F, 0x0328}, + { 0x1E54, 0x0050, 0x0301}, + { 0x1E56, 0x0050, 0x0307}, + { 0x0154, 0x0052, 0x0301}, + { 0x1E58, 0x0052, 0x0307}, + { 0x0158, 0x0052, 0x030C}, + { 0x0210, 0x0052, 0x030F}, + { 0x0212, 0x0052, 0x0311}, + { 0x1E5A, 0x0052, 0x0323}, + { 0x0156, 0x0052, 0x0327}, + { 0x1E5E, 0x0052, 0x0331}, + { 0x015A, 0x0053, 0x0301}, + { 0x015C, 0x0053, 0x0302}, + { 0x1E60, 0x0053, 0x0307}, + { 0x0160, 0x0053, 0x030C}, + { 0x1E62, 0x0053, 0x0323}, + { 0x0218, 0x0053, 0x0326}, + { 0x015E, 0x0053, 0x0327}, + { 0x1E6A, 0x0054, 0x0307}, + { 0x0164, 0x0054, 0x030C}, + { 0x1E6C, 0x0054, 0x0323}, + { 0x021A, 0x0054, 0x0326}, + { 0x0162, 0x0054, 0x0327}, + { 0x1E70, 0x0054, 0x032D}, + { 0x1E6E, 0x0054, 0x0331}, + { 0x00D9, 0x0055, 0x0300}, + { 0x00DA, 0x0055, 0x0301}, + { 0x00DB, 0x0055, 0x0302}, + { 0x0168, 0x0055, 0x0303}, + { 0x016A, 0x0055, 0x0304}, + { 0x016C, 0x0055, 0x0306}, + { 0x00DC, 0x0055, 0x0308}, + { 0x1EE6, 0x0055, 0x0309}, + { 0x016E, 0x0055, 0x030A}, + { 0x0170, 0x0055, 0x030B}, + { 0x01D3, 0x0055, 0x030C}, + { 0x0214, 0x0055, 0x030F}, + { 0x0216, 0x0055, 0x0311}, + { 0x01AF, 0x0055, 0x031B}, + { 0x1EE4, 0x0055, 0x0323}, + { 0x1E72, 0x0055, 0x0324}, + { 0x0172, 0x0055, 0x0328}, + { 0x1E76, 0x0055, 0x032D}, + { 0x1E74, 0x0055, 0x0330}, + { 0x1E7C, 0x0056, 0x0303}, + { 0x1E7E, 0x0056, 0x0323}, + { 0x1E80, 0x0057, 0x0300}, + { 0x1E82, 0x0057, 0x0301}, + { 0x0174, 0x0057, 0x0302}, + { 0x1E86, 0x0057, 0x0307}, + { 0x1E84, 0x0057, 0x0308}, + { 0x1E88, 0x0057, 0x0323}, + { 0x1E8A, 0x0058, 0x0307}, + { 0x1E8C, 0x0058, 0x0308}, + { 0x1EF2, 0x0059, 0x0300}, + { 0x00DD, 0x0059, 0x0301}, + { 0x0176, 0x0059, 0x0302}, + { 0x1EF8, 0x0059, 0x0303}, + { 0x0232, 0x0059, 0x0304}, + { 0x1E8E, 0x0059, 0x0307}, + { 0x0178, 0x0059, 0x0308}, + { 0x1EF6, 0x0059, 0x0309}, + { 0x1EF4, 0x0059, 0x0323}, + { 0x0179, 0x005A, 0x0301}, + { 0x1E90, 0x005A, 0x0302}, + { 0x017B, 0x005A, 0x0307}, + { 0x017D, 0x005A, 0x030C}, + { 0x1E92, 0x005A, 0x0323}, + { 0x1E94, 0x005A, 0x0331}, + { 0x00E0, 0x0061, 0x0300}, + { 0x00E1, 0x0061, 0x0301}, + { 0x00E2, 0x0061, 0x0302}, + { 0x00E3, 0x0061, 0x0303}, + { 0x0101, 0x0061, 0x0304}, + { 0x0103, 0x0061, 0x0306}, + { 0x0227, 0x0061, 0x0307}, + { 0x00E4, 0x0061, 0x0308}, + { 0x1EA3, 0x0061, 0x0309}, + { 0x00E5, 0x0061, 0x030A}, + { 0x01CE, 0x0061, 0x030C}, + { 0x0201, 0x0061, 0x030F}, + { 0x0203, 0x0061, 0x0311}, + { 0x1EA1, 0x0061, 0x0323}, + { 0x1E01, 0x0061, 0x0325}, + { 0x0105, 0x0061, 0x0328}, + { 0x1E03, 0x0062, 0x0307}, + { 0x1E05, 0x0062, 0x0323}, + { 0x1E07, 0x0062, 0x0331}, + { 0x0107, 0x0063, 0x0301}, + { 0x0109, 0x0063, 0x0302}, + { 0x010B, 0x0063, 0x0307}, + { 0x010D, 0x0063, 0x030C}, + { 0x00E7, 0x0063, 0x0327}, + { 0x1E0B, 0x0064, 0x0307}, + { 0x010F, 0x0064, 0x030C}, + { 0x1E0D, 0x0064, 0x0323}, + { 0x1E11, 0x0064, 0x0327}, + { 0x1E13, 0x0064, 0x032D}, + { 0x1E0F, 0x0064, 0x0331}, + { 0x00E8, 0x0065, 0x0300}, + { 0x00E9, 0x0065, 0x0301}, + { 0x00EA, 0x0065, 0x0302}, + { 0x1EBD, 0x0065, 0x0303}, + { 0x0113, 0x0065, 0x0304}, + { 0x0115, 0x0065, 0x0306}, + { 0x0117, 0x0065, 0x0307}, + { 0x00EB, 0x0065, 0x0308}, + { 0x1EBB, 0x0065, 0x0309}, + { 0x011B, 0x0065, 0x030C}, + { 0x0205, 0x0065, 0x030F}, + { 0x0207, 0x0065, 0x0311}, + { 0x1EB9, 0x0065, 0x0323}, + { 0x0229, 0x0065, 0x0327}, + { 0x0119, 0x0065, 0x0328}, + { 0x1E19, 0x0065, 0x032D}, + { 0x1E1B, 0x0065, 0x0330}, + { 0x1E1F, 0x0066, 0x0307}, + { 0x01F5, 0x0067, 0x0301}, + { 0x011D, 0x0067, 0x0302}, + { 0x1E21, 0x0067, 0x0304}, + { 0x011F, 0x0067, 0x0306}, + { 0x0121, 0x0067, 0x0307}, + { 0x01E7, 0x0067, 0x030C}, + { 0x0123, 0x0067, 0x0327}, + { 0x0125, 0x0068, 0x0302}, + { 0x1E23, 0x0068, 0x0307}, + { 0x1E27, 0x0068, 0x0308}, + { 0x021F, 0x0068, 0x030C}, + { 0x1E25, 0x0068, 0x0323}, + { 0x1E29, 0x0068, 0x0327}, + { 0x1E2B, 0x0068, 0x032E}, + { 0x1E96, 0x0068, 0x0331}, + { 0x00EC, 0x0069, 0x0300}, + { 0x00ED, 0x0069, 0x0301}, + { 0x00EE, 0x0069, 0x0302}, + { 0x0129, 0x0069, 0x0303}, + { 0x012B, 0x0069, 0x0304}, + { 0x012D, 0x0069, 0x0306}, + { 0x00EF, 0x0069, 0x0308}, + { 0x1EC9, 0x0069, 0x0309}, + { 0x01D0, 0x0069, 0x030C}, + { 0x0209, 0x0069, 0x030F}, + { 0x020B, 0x0069, 0x0311}, + { 0x1ECB, 0x0069, 0x0323}, + { 0x012F, 0x0069, 0x0328}, + { 0x1E2D, 0x0069, 0x0330}, + { 0x0135, 0x006A, 0x0302}, + { 0x01F0, 0x006A, 0x030C}, + { 0x1E31, 0x006B, 0x0301}, + { 0x01E9, 0x006B, 0x030C}, + { 0x1E33, 0x006B, 0x0323}, + { 0x0137, 0x006B, 0x0327}, + { 0x1E35, 0x006B, 0x0331}, + { 0x013A, 0x006C, 0x0301}, + { 0x013E, 0x006C, 0x030C}, + { 0x1E37, 0x006C, 0x0323}, + { 0x013C, 0x006C, 0x0327}, + { 0x1E3D, 0x006C, 0x032D}, + { 0x1E3B, 0x006C, 0x0331}, + { 0x1E3F, 0x006D, 0x0301}, + { 0x1E41, 0x006D, 0x0307}, + { 0x1E43, 0x006D, 0x0323}, + { 0x01F9, 0x006E, 0x0300}, + { 0x0144, 0x006E, 0x0301}, + { 0x00F1, 0x006E, 0x0303}, + { 0x1E45, 0x006E, 0x0307}, + { 0x0148, 0x006E, 0x030C}, + { 0x1E47, 0x006E, 0x0323}, + { 0x0146, 0x006E, 0x0327}, + { 0x1E4B, 0x006E, 0x032D}, + { 0x1E49, 0x006E, 0x0331}, + { 0x00F2, 0x006F, 0x0300}, + { 0x00F3, 0x006F, 0x0301}, + { 0x00F4, 0x006F, 0x0302}, + { 0x00F5, 0x006F, 0x0303}, + { 0x014D, 0x006F, 0x0304}, + { 0x014F, 0x006F, 0x0306}, + { 0x022F, 0x006F, 0x0307}, + { 0x00F6, 0x006F, 0x0308}, + { 0x1ECF, 0x006F, 0x0309}, + { 0x0151, 0x006F, 0x030B}, + { 0x01D2, 0x006F, 0x030C}, + { 0x020D, 0x006F, 0x030F}, + { 0x020F, 0x006F, 0x0311}, + { 0x01A1, 0x006F, 0x031B}, + { 0x1ECD, 0x006F, 0x0323}, + { 0x01EB, 0x006F, 0x0328}, + { 0x1E55, 0x0070, 0x0301}, + { 0x1E57, 0x0070, 0x0307}, + { 0x0155, 0x0072, 0x0301}, + { 0x1E59, 0x0072, 0x0307}, + { 0x0159, 0x0072, 0x030C}, + { 0x0211, 0x0072, 0x030F}, + { 0x0213, 0x0072, 0x0311}, + { 0x1E5B, 0x0072, 0x0323}, + { 0x0157, 0x0072, 0x0327}, + { 0x1E5F, 0x0072, 0x0331}, + { 0x015B, 0x0073, 0x0301}, + { 0x015D, 0x0073, 0x0302}, + { 0x1E61, 0x0073, 0x0307}, + { 0x0161, 0x0073, 0x030C}, + { 0x1E63, 0x0073, 0x0323}, + { 0x0219, 0x0073, 0x0326}, + { 0x015F, 0x0073, 0x0327}, + { 0x1E6B, 0x0074, 0x0307}, + { 0x1E97, 0x0074, 0x0308}, + { 0x0165, 0x0074, 0x030C}, + { 0x1E6D, 0x0074, 0x0323}, + { 0x021B, 0x0074, 0x0326}, + { 0x0163, 0x0074, 0x0327}, + { 0x1E71, 0x0074, 0x032D}, + { 0x1E6F, 0x0074, 0x0331}, + { 0x00F9, 0x0075, 0x0300}, + { 0x00FA, 0x0075, 0x0301}, + { 0x00FB, 0x0075, 0x0302}, + { 0x0169, 0x0075, 0x0303}, + { 0x016B, 0x0075, 0x0304}, + { 0x016D, 0x0075, 0x0306}, + { 0x00FC, 0x0075, 0x0308}, + { 0x1EE7, 0x0075, 0x0309}, + { 0x016F, 0x0075, 0x030A}, + { 0x0171, 0x0075, 0x030B}, + { 0x01D4, 0x0075, 0x030C}, + { 0x0215, 0x0075, 0x030F}, + { 0x0217, 0x0075, 0x0311}, + { 0x01B0, 0x0075, 0x031B}, + { 0x1EE5, 0x0075, 0x0323}, + { 0x1E73, 0x0075, 0x0324}, + { 0x0173, 0x0075, 0x0328}, + { 0x1E77, 0x0075, 0x032D}, + { 0x1E75, 0x0075, 0x0330}, + { 0x1E7D, 0x0076, 0x0303}, + { 0x1E7F, 0x0076, 0x0323}, + { 0x1E81, 0x0077, 0x0300}, + { 0x1E83, 0x0077, 0x0301}, + { 0x0175, 0x0077, 0x0302}, + { 0x1E87, 0x0077, 0x0307}, + { 0x1E85, 0x0077, 0x0308}, + { 0x1E98, 0x0077, 0x030A}, + { 0x1E89, 0x0077, 0x0323}, + { 0x1E8B, 0x0078, 0x0307}, + { 0x1E8D, 0x0078, 0x0308}, + { 0x1EF3, 0x0079, 0x0300}, + { 0x00FD, 0x0079, 0x0301}, + { 0x0177, 0x0079, 0x0302}, + { 0x1EF9, 0x0079, 0x0303}, + { 0x0233, 0x0079, 0x0304}, + { 0x1E8F, 0x0079, 0x0307}, + { 0x00FF, 0x0079, 0x0308}, + { 0x1EF7, 0x0079, 0x0309}, + { 0x1E99, 0x0079, 0x030A}, + { 0x1EF5, 0x0079, 0x0323}, + { 0x017A, 0x007A, 0x0301}, + { 0x1E91, 0x007A, 0x0302}, + { 0x017C, 0x007A, 0x0307}, + { 0x017E, 0x007A, 0x030C}, + { 0x1E93, 0x007A, 0x0323}, + { 0x1E95, 0x007A, 0x0331}, + { 0x1FED, 0x00A8, 0x0300}, + { 0x0385, 0x00A8, 0x0301}, + { 0x1FC1, 0x00A8, 0x0342}, + { 0x1EA6, 0x00C2, 0x0300}, + { 0x1EA4, 0x00C2, 0x0301}, + { 0x1EAA, 0x00C2, 0x0303}, + { 0x1EA8, 0x00C2, 0x0309}, + { 0x01DE, 0x00C4, 0x0304}, + { 0x01FA, 0x00C5, 0x0301}, + { 0x01FC, 0x00C6, 0x0301}, + { 0x01E2, 0x00C6, 0x0304}, + { 0x1E08, 0x00C7, 0x0301}, + { 0x1EC0, 0x00CA, 0x0300}, + { 0x1EBE, 0x00CA, 0x0301}, + { 0x1EC4, 0x00CA, 0x0303}, + { 0x1EC2, 0x00CA, 0x0309}, + { 0x1E2E, 0x00CF, 0x0301}, + { 0x1ED2, 0x00D4, 0x0300}, + { 0x1ED0, 0x00D4, 0x0301}, + { 0x1ED6, 0x00D4, 0x0303}, + { 0x1ED4, 0x00D4, 0x0309}, + { 0x1E4C, 0x00D5, 0x0301}, + { 0x022C, 0x00D5, 0x0304}, + { 0x1E4E, 0x00D5, 0x0308}, + { 0x022A, 0x00D6, 0x0304}, + { 0x01FE, 0x00D8, 0x0301}, + { 0x01DB, 0x00DC, 0x0300}, + { 0x01D7, 0x00DC, 0x0301}, + { 0x01D5, 0x00DC, 0x0304}, + { 0x01D9, 0x00DC, 0x030C}, + { 0x1EA7, 0x00E2, 0x0300}, + { 0x1EA5, 0x00E2, 0x0301}, + { 0x1EAB, 0x00E2, 0x0303}, + { 0x1EA9, 0x00E2, 0x0309}, + { 0x01DF, 0x00E4, 0x0304}, + { 0x01FB, 0x00E5, 0x0301}, + { 0x01FD, 0x00E6, 0x0301}, + { 0x01E3, 0x00E6, 0x0304}, + { 0x1E09, 0x00E7, 0x0301}, + { 0x1EC1, 0x00EA, 0x0300}, + { 0x1EBF, 0x00EA, 0x0301}, + { 0x1EC5, 0x00EA, 0x0303}, + { 0x1EC3, 0x00EA, 0x0309}, + { 0x1E2F, 0x00EF, 0x0301}, + { 0x1ED3, 0x00F4, 0x0300}, + { 0x1ED1, 0x00F4, 0x0301}, + { 0x1ED7, 0x00F4, 0x0303}, + { 0x1ED5, 0x00F4, 0x0309}, + { 0x1E4D, 0x00F5, 0x0301}, + { 0x022D, 0x00F5, 0x0304}, + { 0x1E4F, 0x00F5, 0x0308}, + { 0x022B, 0x00F6, 0x0304}, + { 0x01FF, 0x00F8, 0x0301}, + { 0x01DC, 0x00FC, 0x0300}, + { 0x01D8, 0x00FC, 0x0301}, + { 0x01D6, 0x00FC, 0x0304}, + { 0x01DA, 0x00FC, 0x030C}, + { 0x1EB0, 0x0102, 0x0300}, + { 0x1EAE, 0x0102, 0x0301}, + { 0x1EB4, 0x0102, 0x0303}, + { 0x1EB2, 0x0102, 0x0309}, + { 0x1EB1, 0x0103, 0x0300}, + { 0x1EAF, 0x0103, 0x0301}, + { 0x1EB5, 0x0103, 0x0303}, + { 0x1EB3, 0x0103, 0x0309}, + { 0x1E14, 0x0112, 0x0300}, + { 0x1E16, 0x0112, 0x0301}, + { 0x1E15, 0x0113, 0x0300}, + { 0x1E17, 0x0113, 0x0301}, + { 0x1E50, 0x014C, 0x0300}, + { 0x1E52, 0x014C, 0x0301}, + { 0x1E51, 0x014D, 0x0300}, + { 0x1E53, 0x014D, 0x0301}, + { 0x1E64, 0x015A, 0x0307}, + { 0x1E65, 0x015B, 0x0307}, + { 0x1E66, 0x0160, 0x0307}, + { 0x1E67, 0x0161, 0x0307}, + { 0x1E78, 0x0168, 0x0301}, + { 0x1E79, 0x0169, 0x0301}, + { 0x1E7A, 0x016A, 0x0308}, + { 0x1E7B, 0x016B, 0x0308}, + { 0x1E9B, 0x017F, 0x0307}, + { 0x1EDC, 0x01A0, 0x0300}, + { 0x1EDA, 0x01A0, 0x0301}, + { 0x1EE0, 0x01A0, 0x0303}, + { 0x1EDE, 0x01A0, 0x0309}, + { 0x1EE2, 0x01A0, 0x0323}, + { 0x1EDD, 0x01A1, 0x0300}, + { 0x1EDB, 0x01A1, 0x0301}, + { 0x1EE1, 0x01A1, 0x0303}, + { 0x1EDF, 0x01A1, 0x0309}, + { 0x1EE3, 0x01A1, 0x0323}, + { 0x1EEA, 0x01AF, 0x0300}, + { 0x1EE8, 0x01AF, 0x0301}, + { 0x1EEE, 0x01AF, 0x0303}, + { 0x1EEC, 0x01AF, 0x0309}, + { 0x1EF0, 0x01AF, 0x0323}, + { 0x1EEB, 0x01B0, 0x0300}, + { 0x1EE9, 0x01B0, 0x0301}, + { 0x1EEF, 0x01B0, 0x0303}, + { 0x1EED, 0x01B0, 0x0309}, + { 0x1EF1, 0x01B0, 0x0323}, + { 0x01EE, 0x01B7, 0x030C}, + { 0x01EC, 0x01EA, 0x0304}, + { 0x01ED, 0x01EB, 0x0304}, + { 0x01E0, 0x0226, 0x0304}, + { 0x01E1, 0x0227, 0x0304}, + { 0x1E1C, 0x0228, 0x0306}, + { 0x1E1D, 0x0229, 0x0306}, + { 0x0230, 0x022E, 0x0304}, + { 0x0231, 0x022F, 0x0304}, + { 0x01EF, 0x0292, 0x030C}, + { 0x0344, 0x0308, 0x0301}, + { 0x1FBA, 0x0391, 0x0300}, + { 0x0386, 0x0391, 0x0301}, + { 0x1FB9, 0x0391, 0x0304}, + { 0x1FB8, 0x0391, 0x0306}, + { 0x1F08, 0x0391, 0x0313}, + { 0x1F09, 0x0391, 0x0314}, + { 0x1FBC, 0x0391, 0x0345}, + { 0x1FC8, 0x0395, 0x0300}, + { 0x0388, 0x0395, 0x0301}, + { 0x1F18, 0x0395, 0x0313}, + { 0x1F19, 0x0395, 0x0314}, + { 0x1FCA, 0x0397, 0x0300}, + { 0x0389, 0x0397, 0x0301}, + { 0x1F28, 0x0397, 0x0313}, + { 0x1F29, 0x0397, 0x0314}, + { 0x1FCC, 0x0397, 0x0345}, + { 0x1FDA, 0x0399, 0x0300}, + { 0x038A, 0x0399, 0x0301}, + { 0x1FD9, 0x0399, 0x0304}, + { 0x1FD8, 0x0399, 0x0306}, + { 0x03AA, 0x0399, 0x0308}, + { 0x1F38, 0x0399, 0x0313}, + { 0x1F39, 0x0399, 0x0314}, + { 0x1FF8, 0x039F, 0x0300}, + { 0x038C, 0x039F, 0x0301}, + { 0x1F48, 0x039F, 0x0313}, + { 0x1F49, 0x039F, 0x0314}, + { 0x1FEC, 0x03A1, 0x0314}, + { 0x1FEA, 0x03A5, 0x0300}, + { 0x038E, 0x03A5, 0x0301}, + { 0x1FE9, 0x03A5, 0x0304}, + { 0x1FE8, 0x03A5, 0x0306}, + { 0x03AB, 0x03A5, 0x0308}, + { 0x1F59, 0x03A5, 0x0314}, + { 0x1FFA, 0x03A9, 0x0300}, + { 0x038F, 0x03A9, 0x0301}, + { 0x1F68, 0x03A9, 0x0313}, + { 0x1F69, 0x03A9, 0x0314}, + { 0x1FFC, 0x03A9, 0x0345}, + { 0x1FB4, 0x03AC, 0x0345}, + { 0x1FC4, 0x03AE, 0x0345}, + { 0x1F70, 0x03B1, 0x0300}, + { 0x03AC, 0x03B1, 0x0301}, + { 0x1FB1, 0x03B1, 0x0304}, + { 0x1FB0, 0x03B1, 0x0306}, + { 0x1F00, 0x03B1, 0x0313}, + { 0x1F01, 0x03B1, 0x0314}, + { 0x1FB6, 0x03B1, 0x0342}, + { 0x1FB3, 0x03B1, 0x0345}, + { 0x1F72, 0x03B5, 0x0300}, + { 0x03AD, 0x03B5, 0x0301}, + { 0x1F10, 0x03B5, 0x0313}, + { 0x1F11, 0x03B5, 0x0314}, + { 0x1F74, 0x03B7, 0x0300}, + { 0x03AE, 0x03B7, 0x0301}, + { 0x1F20, 0x03B7, 0x0313}, + { 0x1F21, 0x03B7, 0x0314}, + { 0x1FC6, 0x03B7, 0x0342}, + { 0x1FC3, 0x03B7, 0x0345}, + { 0x1F76, 0x03B9, 0x0300}, + { 0x03AF, 0x03B9, 0x0301}, + { 0x1FD1, 0x03B9, 0x0304}, + { 0x1FD0, 0x03B9, 0x0306}, + { 0x03CA, 0x03B9, 0x0308}, + { 0x1F30, 0x03B9, 0x0313}, + { 0x1F31, 0x03B9, 0x0314}, + { 0x1FD6, 0x03B9, 0x0342}, + { 0x1F78, 0x03BF, 0x0300}, + { 0x03CC, 0x03BF, 0x0301}, + { 0x1F40, 0x03BF, 0x0313}, + { 0x1F41, 0x03BF, 0x0314}, + { 0x1FE4, 0x03C1, 0x0313}, + { 0x1FE5, 0x03C1, 0x0314}, + { 0x1F7A, 0x03C5, 0x0300}, + { 0x03CD, 0x03C5, 0x0301}, + { 0x1FE1, 0x03C5, 0x0304}, + { 0x1FE0, 0x03C5, 0x0306}, + { 0x03CB, 0x03C5, 0x0308}, + { 0x1F50, 0x03C5, 0x0313}, + { 0x1F51, 0x03C5, 0x0314}, + { 0x1FE6, 0x03C5, 0x0342}, + { 0x1F7C, 0x03C9, 0x0300}, + { 0x03CE, 0x03C9, 0x0301}, + { 0x1F60, 0x03C9, 0x0313}, + { 0x1F61, 0x03C9, 0x0314}, + { 0x1FF6, 0x03C9, 0x0342}, + { 0x1FF3, 0x03C9, 0x0345}, + { 0x1FD2, 0x03CA, 0x0300}, + { 0x0390, 0x03CA, 0x0301}, + { 0x1FD7, 0x03CA, 0x0342}, + { 0x1FE2, 0x03CB, 0x0300}, + { 0x03B0, 0x03CB, 0x0301}, + { 0x1FE7, 0x03CB, 0x0342}, + { 0x1FF4, 0x03CE, 0x0345}, + { 0x03D3, 0x03D2, 0x0301}, + { 0x03D4, 0x03D2, 0x0308}, + { 0x0407, 0x0406, 0x0308}, + { 0x04D0, 0x0410, 0x0306}, + { 0x04D2, 0x0410, 0x0308}, + { 0x0403, 0x0413, 0x0301}, + { 0x0400, 0x0415, 0x0300}, + { 0x04D6, 0x0415, 0x0306}, + { 0x0401, 0x0415, 0x0308}, + { 0x04C1, 0x0416, 0x0306}, + { 0x04DC, 0x0416, 0x0308}, + { 0x04DE, 0x0417, 0x0308}, + { 0x040D, 0x0418, 0x0300}, + { 0x04E2, 0x0418, 0x0304}, + { 0x0419, 0x0418, 0x0306}, + { 0x04E4, 0x0418, 0x0308}, + { 0x040C, 0x041A, 0x0301}, + { 0x04E6, 0x041E, 0x0308}, + { 0x04EE, 0x0423, 0x0304}, + { 0x040E, 0x0423, 0x0306}, + { 0x04F0, 0x0423, 0x0308}, + { 0x04F2, 0x0423, 0x030B}, + { 0x04F4, 0x0427, 0x0308}, + { 0x04F8, 0x042B, 0x0308}, + { 0x04EC, 0x042D, 0x0308}, + { 0x04D1, 0x0430, 0x0306}, + { 0x04D3, 0x0430, 0x0308}, + { 0x0453, 0x0433, 0x0301}, + { 0x0450, 0x0435, 0x0300}, + { 0x04D7, 0x0435, 0x0306}, + { 0x0451, 0x0435, 0x0308}, + { 0x04C2, 0x0436, 0x0306}, + { 0x04DD, 0x0436, 0x0308}, + { 0x04DF, 0x0437, 0x0308}, + { 0x045D, 0x0438, 0x0300}, + { 0x04E3, 0x0438, 0x0304}, + { 0x0439, 0x0438, 0x0306}, + { 0x04E5, 0x0438, 0x0308}, + { 0x045C, 0x043A, 0x0301}, + { 0x04E7, 0x043E, 0x0308}, + { 0x04EF, 0x0443, 0x0304}, + { 0x045E, 0x0443, 0x0306}, + { 0x04F1, 0x0443, 0x0308}, + { 0x04F3, 0x0443, 0x030B}, + { 0x04F5, 0x0447, 0x0308}, + { 0x04F9, 0x044B, 0x0308}, + { 0x04ED, 0x044D, 0x0308}, + { 0x0457, 0x0456, 0x0308}, + { 0x0476, 0x0474, 0x030F}, + { 0x0477, 0x0475, 0x030F}, + { 0x04DA, 0x04D8, 0x0308}, + { 0x04DB, 0x04D9, 0x0308}, + { 0x04EA, 0x04E8, 0x0308}, + { 0x04EB, 0x04E9, 0x0308}, + { 0xFB2E, 0x05D0, 0x05B7}, + { 0xFB2F, 0x05D0, 0x05B8}, + { 0xFB30, 0x05D0, 0x05BC}, + { 0xFB31, 0x05D1, 0x05BC}, + { 0xFB4C, 0x05D1, 0x05BF}, + { 0xFB32, 0x05D2, 0x05BC}, + { 0xFB33, 0x05D3, 0x05BC}, + { 0xFB34, 0x05D4, 0x05BC}, + { 0xFB4B, 0x05D5, 0x05B9}, + { 0xFB35, 0x05D5, 0x05BC}, + { 0xFB36, 0x05D6, 0x05BC}, + { 0xFB38, 0x05D8, 0x05BC}, + { 0xFB1D, 0x05D9, 0x05B4}, + { 0xFB39, 0x05D9, 0x05BC}, + { 0xFB3A, 0x05DA, 0x05BC}, + { 0xFB3B, 0x05DB, 0x05BC}, + { 0xFB4D, 0x05DB, 0x05BF}, + { 0xFB3C, 0x05DC, 0x05BC}, + { 0xFB3E, 0x05DE, 0x05BC}, + { 0xFB40, 0x05E0, 0x05BC}, + { 0xFB41, 0x05E1, 0x05BC}, + { 0xFB43, 0x05E3, 0x05BC}, + { 0xFB44, 0x05E4, 0x05BC}, + { 0xFB4E, 0x05E4, 0x05BF}, + { 0xFB46, 0x05E6, 0x05BC}, + { 0xFB47, 0x05E7, 0x05BC}, + { 0xFB48, 0x05E8, 0x05BC}, + { 0xFB49, 0x05E9, 0x05BC}, + { 0xFB2A, 0x05E9, 0x05C1}, + { 0xFB2B, 0x05E9, 0x05C2}, + { 0xFB4A, 0x05EA, 0x05BC}, + { 0xFB1F, 0x05F2, 0x05B7}, + { 0x0622, 0x0627, 0x0653}, + { 0x0623, 0x0627, 0x0654}, + { 0x0625, 0x0627, 0x0655}, + { 0x0624, 0x0648, 0x0654}, + { 0x0626, 0x064A, 0x0654}, + { 0x06C2, 0x06C1, 0x0654}, + { 0x06D3, 0x06D2, 0x0654}, + { 0x06C0, 0x06D5, 0x0654}, + { 0x0958, 0x0915, 0x093C}, + { 0x0959, 0x0916, 0x093C}, + { 0x095A, 0x0917, 0x093C}, + { 0x095B, 0x091C, 0x093C}, + { 0x095C, 0x0921, 0x093C}, + { 0x095D, 0x0922, 0x093C}, + { 0x0929, 0x0928, 0x093C}, + { 0x095E, 0x092B, 0x093C}, + { 0x095F, 0x092F, 0x093C}, + { 0x0931, 0x0930, 0x093C}, + { 0x0934, 0x0933, 0x093C}, + { 0x09DC, 0x09A1, 0x09BC}, + { 0x09DD, 0x09A2, 0x09BC}, + { 0x09DF, 0x09AF, 0x09BC}, + { 0x09CB, 0x09C7, 0x09BE}, + { 0x09CC, 0x09C7, 0x09D7}, + { 0x0A59, 0x0A16, 0x0A3C}, + { 0x0A5A, 0x0A17, 0x0A3C}, + { 0x0A5B, 0x0A1C, 0x0A3C}, + { 0x0A5E, 0x0A2B, 0x0A3C}, + { 0x0A33, 0x0A32, 0x0A3C}, + { 0x0A36, 0x0A38, 0x0A3C}, + { 0x0B5C, 0x0B21, 0x0B3C}, + { 0x0B5D, 0x0B22, 0x0B3C}, + { 0x0B4B, 0x0B47, 0x0B3E}, + { 0x0B48, 0x0B47, 0x0B56}, + { 0x0B4C, 0x0B47, 0x0B57}, + { 0x0B94, 0x0B92, 0x0BD7}, + { 0x0BCA, 0x0BC6, 0x0BBE}, + { 0x0BCC, 0x0BC6, 0x0BD7}, + { 0x0BCB, 0x0BC7, 0x0BBE}, + { 0x0C48, 0x0C46, 0x0C56}, + { 0x0CC0, 0x0CBF, 0x0CD5}, + { 0x0CCA, 0x0CC6, 0x0CC2}, + { 0x0CC7, 0x0CC6, 0x0CD5}, + { 0x0CC8, 0x0CC6, 0x0CD6}, + { 0x0CCB, 0x0CCA, 0x0CD5}, + { 0x0D4A, 0x0D46, 0x0D3E}, + { 0x0D4C, 0x0D46, 0x0D57}, + { 0x0D4B, 0x0D47, 0x0D3E}, + { 0x0DDA, 0x0DD9, 0x0DCA}, + { 0x0DDC, 0x0DD9, 0x0DCF}, + { 0x0DDE, 0x0DD9, 0x0DDF}, + { 0x0DDD, 0x0DDC, 0x0DCA}, + { 0x0F69, 0x0F40, 0x0FB5}, + { 0x0F43, 0x0F42, 0x0FB7}, + { 0x0F4D, 0x0F4C, 0x0FB7}, + { 0x0F52, 0x0F51, 0x0FB7}, + { 0x0F57, 0x0F56, 0x0FB7}, + { 0x0F5C, 0x0F5B, 0x0FB7}, + { 0x0F73, 0x0F71, 0x0F72}, + { 0x0F75, 0x0F71, 0x0F74}, + { 0x0F81, 0x0F71, 0x0F80}, + { 0x0FB9, 0x0F90, 0x0FB5}, + { 0x0F93, 0x0F92, 0x0FB7}, + { 0x0F9D, 0x0F9C, 0x0FB7}, + { 0x0FA2, 0x0FA1, 0x0FB7}, + { 0x0FA7, 0x0FA6, 0x0FB7}, + { 0x0FAC, 0x0FAB, 0x0FB7}, + { 0x0F76, 0x0FB2, 0x0F80}, + { 0x0F78, 0x0FB3, 0x0F80}, + { 0x1026, 0x1025, 0x102E}, + { 0x1B06, 0x1B05, 0x1B35}, + { 0x1B08, 0x1B07, 0x1B35}, + { 0x1B0A, 0x1B09, 0x1B35}, + { 0x1B0C, 0x1B0B, 0x1B35}, + { 0x1B0E, 0x1B0D, 0x1B35}, + { 0x1B12, 0x1B11, 0x1B35}, + { 0x1B3B, 0x1B3A, 0x1B35}, + { 0x1B3D, 0x1B3C, 0x1B35}, + { 0x1B40, 0x1B3E, 0x1B35}, + { 0x1B41, 0x1B3F, 0x1B35}, + { 0x1B43, 0x1B42, 0x1B35}, + { 0x1E38, 0x1E36, 0x0304}, + { 0x1E39, 0x1E37, 0x0304}, + { 0x1E5C, 0x1E5A, 0x0304}, + { 0x1E5D, 0x1E5B, 0x0304}, + { 0x1E68, 0x1E62, 0x0307}, + { 0x1E69, 0x1E63, 0x0307}, + { 0x1EAC, 0x1EA0, 0x0302}, + { 0x1EB6, 0x1EA0, 0x0306}, + { 0x1EAD, 0x1EA1, 0x0302}, + { 0x1EB7, 0x1EA1, 0x0306}, + { 0x1EC6, 0x1EB8, 0x0302}, + { 0x1EC7, 0x1EB9, 0x0302}, + { 0x1ED8, 0x1ECC, 0x0302}, + { 0x1ED9, 0x1ECD, 0x0302}, + { 0x1F02, 0x1F00, 0x0300}, + { 0x1F04, 0x1F00, 0x0301}, + { 0x1F06, 0x1F00, 0x0342}, + { 0x1F80, 0x1F00, 0x0345}, + { 0x1F03, 0x1F01, 0x0300}, + { 0x1F05, 0x1F01, 0x0301}, + { 0x1F07, 0x1F01, 0x0342}, + { 0x1F81, 0x1F01, 0x0345}, + { 0x1F82, 0x1F02, 0x0345}, + { 0x1F83, 0x1F03, 0x0345}, + { 0x1F84, 0x1F04, 0x0345}, + { 0x1F85, 0x1F05, 0x0345}, + { 0x1F86, 0x1F06, 0x0345}, + { 0x1F87, 0x1F07, 0x0345}, + { 0x1F0A, 0x1F08, 0x0300}, + { 0x1F0C, 0x1F08, 0x0301}, + { 0x1F0E, 0x1F08, 0x0342}, + { 0x1F88, 0x1F08, 0x0345}, + { 0x1F0B, 0x1F09, 0x0300}, + { 0x1F0D, 0x1F09, 0x0301}, + { 0x1F0F, 0x1F09, 0x0342}, + { 0x1F89, 0x1F09, 0x0345}, + { 0x1F8A, 0x1F0A, 0x0345}, + { 0x1F8B, 0x1F0B, 0x0345}, + { 0x1F8C, 0x1F0C, 0x0345}, + { 0x1F8D, 0x1F0D, 0x0345}, + { 0x1F8E, 0x1F0E, 0x0345}, + { 0x1F8F, 0x1F0F, 0x0345}, + { 0x1F12, 0x1F10, 0x0300}, + { 0x1F14, 0x1F10, 0x0301}, + { 0x1F13, 0x1F11, 0x0300}, + { 0x1F15, 0x1F11, 0x0301}, + { 0x1F1A, 0x1F18, 0x0300}, + { 0x1F1C, 0x1F18, 0x0301}, + { 0x1F1B, 0x1F19, 0x0300}, + { 0x1F1D, 0x1F19, 0x0301}, + { 0x1F22, 0x1F20, 0x0300}, + { 0x1F24, 0x1F20, 0x0301}, + { 0x1F26, 0x1F20, 0x0342}, + { 0x1F90, 0x1F20, 0x0345}, + { 0x1F23, 0x1F21, 0x0300}, + { 0x1F25, 0x1F21, 0x0301}, + { 0x1F27, 0x1F21, 0x0342}, + { 0x1F91, 0x1F21, 0x0345}, + { 0x1F92, 0x1F22, 0x0345}, + { 0x1F93, 0x1F23, 0x0345}, + { 0x1F94, 0x1F24, 0x0345}, + { 0x1F95, 0x1F25, 0x0345}, + { 0x1F96, 0x1F26, 0x0345}, + { 0x1F97, 0x1F27, 0x0345}, + { 0x1F2A, 0x1F28, 0x0300}, + { 0x1F2C, 0x1F28, 0x0301}, + { 0x1F2E, 0x1F28, 0x0342}, + { 0x1F98, 0x1F28, 0x0345}, + { 0x1F2B, 0x1F29, 0x0300}, + { 0x1F2D, 0x1F29, 0x0301}, + { 0x1F2F, 0x1F29, 0x0342}, + { 0x1F99, 0x1F29, 0x0345}, + { 0x1F9A, 0x1F2A, 0x0345}, + { 0x1F9B, 0x1F2B, 0x0345}, + { 0x1F9C, 0x1F2C, 0x0345}, + { 0x1F9D, 0x1F2D, 0x0345}, + { 0x1F9E, 0x1F2E, 0x0345}, + { 0x1F9F, 0x1F2F, 0x0345}, + { 0x1F32, 0x1F30, 0x0300}, + { 0x1F34, 0x1F30, 0x0301}, + { 0x1F36, 0x1F30, 0x0342}, + { 0x1F33, 0x1F31, 0x0300}, + { 0x1F35, 0x1F31, 0x0301}, + { 0x1F37, 0x1F31, 0x0342}, + { 0x1F3A, 0x1F38, 0x0300}, + { 0x1F3C, 0x1F38, 0x0301}, + { 0x1F3E, 0x1F38, 0x0342}, + { 0x1F3B, 0x1F39, 0x0300}, + { 0x1F3D, 0x1F39, 0x0301}, + { 0x1F3F, 0x1F39, 0x0342}, + { 0x1F42, 0x1F40, 0x0300}, + { 0x1F44, 0x1F40, 0x0301}, + { 0x1F43, 0x1F41, 0x0300}, + { 0x1F45, 0x1F41, 0x0301}, + { 0x1F4A, 0x1F48, 0x0300}, + { 0x1F4C, 0x1F48, 0x0301}, + { 0x1F4B, 0x1F49, 0x0300}, + { 0x1F4D, 0x1F49, 0x0301}, + { 0x1F52, 0x1F50, 0x0300}, + { 0x1F54, 0x1F50, 0x0301}, + { 0x1F56, 0x1F50, 0x0342}, + { 0x1F53, 0x1F51, 0x0300}, + { 0x1F55, 0x1F51, 0x0301}, + { 0x1F57, 0x1F51, 0x0342}, + { 0x1F5B, 0x1F59, 0x0300}, + { 0x1F5D, 0x1F59, 0x0301}, + { 0x1F5F, 0x1F59, 0x0342}, + { 0x1F62, 0x1F60, 0x0300}, + { 0x1F64, 0x1F60, 0x0301}, + { 0x1F66, 0x1F60, 0x0342}, + { 0x1FA0, 0x1F60, 0x0345}, + { 0x1F63, 0x1F61, 0x0300}, + { 0x1F65, 0x1F61, 0x0301}, + { 0x1F67, 0x1F61, 0x0342}, + { 0x1FA1, 0x1F61, 0x0345}, + { 0x1FA2, 0x1F62, 0x0345}, + { 0x1FA3, 0x1F63, 0x0345}, + { 0x1FA4, 0x1F64, 0x0345}, + { 0x1FA5, 0x1F65, 0x0345}, + { 0x1FA6, 0x1F66, 0x0345}, + { 0x1FA7, 0x1F67, 0x0345}, + { 0x1F6A, 0x1F68, 0x0300}, + { 0x1F6C, 0x1F68, 0x0301}, + { 0x1F6E, 0x1F68, 0x0342}, + { 0x1FA8, 0x1F68, 0x0345}, + { 0x1F6B, 0x1F69, 0x0300}, + { 0x1F6D, 0x1F69, 0x0301}, + { 0x1F6F, 0x1F69, 0x0342}, + { 0x1FA9, 0x1F69, 0x0345}, + { 0x1FAA, 0x1F6A, 0x0345}, + { 0x1FAB, 0x1F6B, 0x0345}, + { 0x1FAC, 0x1F6C, 0x0345}, + { 0x1FAD, 0x1F6D, 0x0345}, + { 0x1FAE, 0x1F6E, 0x0345}, + { 0x1FAF, 0x1F6F, 0x0345}, + { 0x1FB2, 0x1F70, 0x0345}, + { 0x1FC2, 0x1F74, 0x0345}, + { 0x1FF2, 0x1F7C, 0x0345}, + { 0x1FB7, 0x1FB6, 0x0345}, + { 0x1FCD, 0x1FBF, 0x0300}, + { 0x1FCE, 0x1FBF, 0x0301}, + { 0x1FCF, 0x1FBF, 0x0342}, + { 0x1FC7, 0x1FC6, 0x0345}, + { 0x1FF7, 0x1FF6, 0x0345}, + { 0x1FDD, 0x1FFE, 0x0300}, + { 0x1FDE, 0x1FFE, 0x0301}, + { 0x1FDF, 0x1FFE, 0x0342}, + { 0x219A, 0x2190, 0x0338}, + { 0x219B, 0x2192, 0x0338}, + { 0x21AE, 0x2194, 0x0338}, + { 0x21CD, 0x21D0, 0x0338}, + { 0x21CF, 0x21D2, 0x0338}, + { 0x21CE, 0x21D4, 0x0338}, + { 0x2204, 0x2203, 0x0338}, + { 0x2209, 0x2208, 0x0338}, + { 0x220C, 0x220B, 0x0338}, + { 0x2224, 0x2223, 0x0338}, + { 0x2226, 0x2225, 0x0338}, + { 0x2241, 0x223C, 0x0338}, + { 0x2244, 0x2243, 0x0338}, + { 0x2247, 0x2245, 0x0338}, + { 0x2249, 0x2248, 0x0338}, + { 0x226D, 0x224D, 0x0338}, + { 0x2262, 0x2261, 0x0338}, + { 0x2270, 0x2264, 0x0338}, + { 0x2271, 0x2265, 0x0338}, + { 0x2274, 0x2272, 0x0338}, + { 0x2275, 0x2273, 0x0338}, + { 0x2278, 0x2276, 0x0338}, + { 0x2279, 0x2277, 0x0338}, + { 0x2280, 0x227A, 0x0338}, + { 0x2281, 0x227B, 0x0338}, + { 0x22E0, 0x227C, 0x0338}, + { 0x22E1, 0x227D, 0x0338}, + { 0x2284, 0x2282, 0x0338}, + { 0x2285, 0x2283, 0x0338}, + { 0x2288, 0x2286, 0x0338}, + { 0x2289, 0x2287, 0x0338}, + { 0x22E2, 0x2291, 0x0338}, + { 0x22E3, 0x2292, 0x0338}, + { 0x22AC, 0x22A2, 0x0338}, + { 0x22AD, 0x22A8, 0x0338}, + { 0x22AE, 0x22A9, 0x0338}, + { 0x22AF, 0x22AB, 0x0338}, + { 0x22EA, 0x22B2, 0x0338}, + { 0x22EB, 0x22B3, 0x0338}, + { 0x22EC, 0x22B4, 0x0338}, + { 0x22ED, 0x22B5, 0x0338}, + { 0x2ADC, 0x2ADD, 0x0338}, + { 0x3094, 0x3046, 0x3099}, + { 0x304C, 0x304B, 0x3099}, + { 0x304E, 0x304D, 0x3099}, + { 0x3050, 0x304F, 0x3099}, + { 0x3052, 0x3051, 0x3099}, + { 0x3054, 0x3053, 0x3099}, + { 0x3056, 0x3055, 0x3099}, + { 0x3058, 0x3057, 0x3099}, + { 0x305A, 0x3059, 0x3099}, + { 0x305C, 0x305B, 0x3099}, + { 0x305E, 0x305D, 0x3099}, + { 0x3060, 0x305F, 0x3099}, + { 0x3062, 0x3061, 0x3099}, + { 0x3065, 0x3064, 0x3099}, + { 0x3067, 0x3066, 0x3099}, + { 0x3069, 0x3068, 0x3099}, + { 0x3070, 0x306F, 0x3099}, + { 0x3071, 0x306F, 0x309A}, + { 0x3073, 0x3072, 0x3099}, + { 0x3074, 0x3072, 0x309A}, + { 0x3076, 0x3075, 0x3099}, + { 0x3077, 0x3075, 0x309A}, + { 0x3079, 0x3078, 0x3099}, + { 0x307A, 0x3078, 0x309A}, + { 0x307C, 0x307B, 0x3099}, + { 0x307D, 0x307B, 0x309A}, + { 0x309E, 0x309D, 0x3099}, + { 0x30F4, 0x30A6, 0x3099}, + { 0x30AC, 0x30AB, 0x3099}, + { 0x30AE, 0x30AD, 0x3099}, + { 0x30B0, 0x30AF, 0x3099}, + { 0x30B2, 0x30B1, 0x3099}, + { 0x30B4, 0x30B3, 0x3099}, + { 0x30B6, 0x30B5, 0x3099}, + { 0x30B8, 0x30B7, 0x3099}, + { 0x30BA, 0x30B9, 0x3099}, + { 0x30BC, 0x30BB, 0x3099}, + { 0x30BE, 0x30BD, 0x3099}, + { 0x30C0, 0x30BF, 0x3099}, + { 0x30C2, 0x30C1, 0x3099}, + { 0x30C5, 0x30C4, 0x3099}, + { 0x30C7, 0x30C6, 0x3099}, + { 0x30C9, 0x30C8, 0x3099}, + { 0x30D0, 0x30CF, 0x3099}, + { 0x30D1, 0x30CF, 0x309A}, + { 0x30D3, 0x30D2, 0x3099}, + { 0x30D4, 0x30D2, 0x309A}, + { 0x30D6, 0x30D5, 0x3099}, + { 0x30D7, 0x30D5, 0x309A}, + { 0x30D9, 0x30D8, 0x3099}, + { 0x30DA, 0x30D8, 0x309A}, + { 0x30DC, 0x30DB, 0x3099}, + { 0x30DD, 0x30DB, 0x309A}, + { 0x30F7, 0x30EF, 0x3099}, + { 0x30F8, 0x30F0, 0x3099}, + { 0x30F9, 0x30F1, 0x3099}, + { 0x30FA, 0x30F2, 0x3099}, + { 0x30FE, 0x30FD, 0x3099}, + { 0xFB2C, 0xFB49, 0x05C1}, + { 0xFB2D, 0xFB49, 0x05C2}, + }; + + private static final int UNICODE_SHIFT = 21; + + public static char precompose(char base, char comb) { + int min = 0; + int max = precompositions.length - 1; + int mid; + + long sought = base << UNICODE_SHIFT | comb; + long that; + + while (max >= min) { + mid = (min + max) / 2; + that = precompositions[mid][1] << UNICODE_SHIFT | precompositions[mid][2]; + if (that < sought) + min = mid + 1; + else if (that > sought) + max = mid - 1; + else + return precompositions[mid][0]; + } + + // No match; return character without combiner + return base; + } +} diff --git a/app/src/main/java/de/mud/terminal/VDUBuffer.java b/app/src/main/java/de/mud/terminal/VDUBuffer.java new file mode 100644 index 0000000..93e3ccf --- /dev/null +++ b/app/src/main/java/de/mud/terminal/VDUBuffer.java @@ -0,0 +1,854 @@ +/* + * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". + * + * (c) Matthias L. Jugel, Marcus Mei�ner 1996-2005. All Rights Reserved. + * + * Please visit http://javatelnet.org/ for updates and contact. + * + * --LICENSE NOTICE-- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + * --LICENSE NOTICE-- + * + */ + +package de.mud.terminal; + +import java.util.Arrays; + +/** + * Implementation of a Video Display Unit (VDU) buffer. This class contains + * all methods to manipulate the buffer that stores characters and their + * attributes as well as the regions displayed. + * + * @author Matthias L. Jugel, Marcus Meißner + * @version $Id: VDUBuffer.java 503 2005-10-24 07:34:13Z marcus $ + */ +public class VDUBuffer { + + /** The current version id tag */ + public final static String ID = "$Id: VDUBuffer.java 503 2005-10-24 07:34:13Z marcus $"; + + /** Enable debug messages. */ + public final static int debug = 0; + + public int height, width; /* rows and columns */ + public boolean[] update; /* contains the lines that need update */ + public char[][] charArray; /* contains the characters */ + public int[][] charAttributes; /* contains character attrs */ + public int bufSize; + public int maxBufSize; /* buffer sizes */ + public int screenBase; /* the actual screen start */ + public int windowBase; /* where the start displaying */ + public int scrollMarker; /* marks the last line inserted */ + + private int topMargin; /* top scroll margin */ + private int bottomMargin; /* bottom scroll margin */ + + // cursor variables + protected boolean showcursor = true; + protected int cursorX, cursorY; + + /** Scroll up when inserting a line. */ + public final static boolean SCROLL_UP = false; + /** Scroll down when inserting a line. */ + public final static boolean SCROLL_DOWN = true; + + /* Attributes bit-field usage: + * + * 8421 8421 8421 8421 8421 8421 8421 8421 + * |||| |||| |||| |||| |||| |||| |||| |||`- Bold + * |||| |||| |||| |||| |||| |||| |||| ||`-- Underline + * |||| |||| |||| |||| |||| |||| |||| |`--- Invert + * |||| |||| |||| |||| |||| |||| |||| `---- Low + * |||| |||| |||| |||| |||| |||| |||`------ Invisible + * |||| |||| |||| |||| ||`+-++++-+++------- Foreground Color + * |||| |||| |`++-++++-++------------------ Background Color + * |||| |||| `----------------------------- Fullwidth character + * `+++-++++------------------------------- Reserved for future use + */ + + /** Make character normal. */ + public final static int NORMAL = 0x00; + /** Make character bold. */ + public final static int BOLD = 0x01; + /** Underline character. */ + public final static int UNDERLINE = 0x02; + /** Invert character. */ + public final static int INVERT = 0x04; + /** Lower intensity character. */ + public final static int LOW = 0x08; + /** Invisible character. */ + public final static int INVISIBLE = 0x10; + /** Unicode full-width character (CJK, et al.) */ + public final static int FULLWIDTH = 0x8000000; + + /** how much to left shift the foreground color */ + public final static int COLOR_FG_SHIFT = 5; + /** how much to left shift the background color */ + public final static int COLOR_BG_SHIFT = 14; + /** color mask */ + public final static int COLOR = 0x7fffe0; /* 0000 0000 0111 1111 1111 1111 1110 0000 */ + /** foreground color mask */ + public final static int COLOR_FG = 0x3fe0; /* 0000 0000 0000 0000 0011 1111 1110 0000 */ + /** background color mask */ + public final static int COLOR_BG = 0x7fc000; /* 0000 0000 0111 1111 1100 0000 0000 0000 */ + + /** + * Create a new video display buffer with the passed width and height in + * characters. + * @param width the length of the character lines + * @param height the amount of lines on the screen + */ + public VDUBuffer(int width, int height) { + // set the display screen size + setScreenSize(width, height, false); + } + + /** + * Create a standard video display buffer with 80 columns and 24 lines. + */ + public VDUBuffer() { + this(80, 24); + } + + /** + * Put a character on the screen with normal font and outline. + * The character previously on that position will be overwritten. + * You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @param ch the character to show on the screen + * @see #insertChar + * @see #deleteChar + * @see #redraw + */ + public void putChar(int c, int l, char ch) { + putChar(c, l, ch, NORMAL); + } + + /** + * Put a character on the screen with specific font and outline. + * The character previously on that position will be overwritten. + * You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @param ch the character to show on the screen + * @param attributes the character attributes + * @see #BOLD + * @see #UNDERLINE + * @see #INVERT + * @see #INVISIBLE + * @see #NORMAL + * @see #LOW + * @see #insertChar + * @see #deleteChar + * @see #redraw + */ + + public void putChar(int c, int l, char ch, int attributes) { + charArray[screenBase + l][c] = ch; + charAttributes[screenBase + l][c] = attributes; + if (l < height) + update[l + 1] = true; + } + + /** + * Get the character at the specified position. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @see #putChar + */ + public char getChar(int c, int l) { + return charArray[screenBase + l][c]; + } + + /** + * Get the attributes for the specified position. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @see #putChar + */ + public int getAttributes(int c, int l) { + return charAttributes[screenBase + l][c]; + } + + /** + * Insert a character at a specific position on the screen. + * All character right to from this position will be moved one to the right. + * You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @param ch the character to insert + * @param attributes the character attributes + * @see #BOLD + * @see #UNDERLINE + * @see #INVERT + * @see #INVISIBLE + * @see #NORMAL + * @see #LOW + * @see #putChar + * @see #deleteChar + * @see #redraw + */ + public void insertChar(int c, int l, char ch, int attributes) { + System.arraycopy(charArray[screenBase + l], c, + charArray[screenBase + l], c + 1, width - c - 1); + System.arraycopy(charAttributes[screenBase + l], c, + charAttributes[screenBase + l], c + 1, width - c - 1); + putChar(c, l, ch, attributes); + } + + /** + * Delete a character at a given position on the screen. + * All characters right to the position will be moved one to the left. + * You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @see #putChar + * @see #insertChar + * @see #redraw + */ + public void deleteChar(int c, int l) { + if (c < width - 1) { + System.arraycopy(charArray[screenBase + l], c + 1, + charArray[screenBase + l], c, width - c - 1); + System.arraycopy(charAttributes[screenBase + l], c + 1, + charAttributes[screenBase + l], c, width - c - 1); + } + putChar(width - 1, l, (char) 0); + } + + /** + * Put a String at a specific position. Any characters previously on that + * position will be overwritten. You need to call redraw() for screen update. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @param s the string to be shown on the screen + * @see #BOLD + * @see #UNDERLINE + * @see #INVERT + * @see #INVISIBLE + * @see #NORMAL + * @see #LOW + * @see #putChar + * @see #insertLine + * @see #deleteLine + * @see #redraw + */ + public void putString(int c, int l, String s) { + putString(c, l, s, NORMAL); + } + + /** + * Put a String at a specific position giving all characters the same + * attributes. Any characters previously on that position will be + * overwritten. You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (line) + * @param s the string to be shown on the screen + * @param attributes character attributes + * @see #BOLD + * @see #UNDERLINE + * @see #INVERT + * @see #INVISIBLE + * @see #NORMAL + * @see #LOW + * @see #putChar + * @see #insertLine + * @see #deleteLine + * @see #redraw + */ + public void putString(int c, int l, String s, int attributes) { + for (int i = 0; i < s.length() && c + i < width; i++) + putChar(c + i, l, s.charAt(i), attributes); + } + + /** + * Insert a blank line at a specific position. + * The current line and all previous lines are scrolled one line up. The + * top line is lost. You need to call redraw() to update the screen. + * @param l the y-coordinate to insert the line + * @see #deleteLine + * @see #redraw + */ + public void insertLine(int l) { + insertLine(l, 1, SCROLL_UP); + } + + /** + * Insert blank lines at a specific position. + * You need to call redraw() to update the screen + * @param l the y-coordinate to insert the line + * @param n amount of lines to be inserted + * @see #deleteLine + * @see #redraw + */ + public void insertLine(int l, int n) { + insertLine(l, n, SCROLL_UP); + } + + /** + * Insert a blank line at a specific position. Scroll text according to + * the argument. + * You need to call redraw() to update the screen + * @param l the y-coordinate to insert the line + * @param scrollDown scroll down + * @see #deleteLine + * @see #SCROLL_UP + * @see #SCROLL_DOWN + * @see #redraw + */ + public void insertLine(int l, boolean scrollDown) { + insertLine(l, 1, scrollDown); + } + + /** + * Insert blank lines at a specific position. + * The current line and all previous lines are scrolled one line up. The + * top line is lost. You need to call redraw() to update the screen. + * @param l the y-coordinate to insert the line + * @param n number of lines to be inserted + * @param scrollDown scroll down + * @see #deleteLine + * @see #SCROLL_UP + * @see #SCROLL_DOWN + * @see #redraw + */ + public synchronized void insertLine(int l, int n, boolean scrollDown) { + char cbuf[][] = null; + int abuf[][] = null; + int offset = 0; + int oldBase = screenBase; + + int newScreenBase = screenBase; + int newWindowBase = windowBase; + int newBufSize = bufSize; + + if (l > bottomMargin) /* We do not scroll below bottom margin (below the scrolling region). */ + return; + int top = (l < topMargin ? + 0 : (l > bottomMargin ? + (bottomMargin + 1 < height ? + bottomMargin + 1 : height - 1) : topMargin)); + int bottom = (l > bottomMargin ? + height - 1 : (l < topMargin ? + (topMargin > 0 ? + topMargin - 1 : 0) : bottomMargin)); + + // System.out.println("l is "+l+", top is "+top+", bottom is "+bottom+", bottomargin is "+bottomMargin+", topMargin is "+topMargin); + + if (scrollDown) { + if (n > (bottom - top)) n = (bottom - top); + int size = bottom - l - (n - 1); + if(size < 0) size = 0; + cbuf = new char[size][]; + abuf = new int[size][]; + + System.arraycopy(charArray, oldBase + l, cbuf, 0, bottom - l - (n - 1)); + System.arraycopy(charAttributes, oldBase + l, + abuf, 0, bottom - l - (n - 1)); + System.arraycopy(cbuf, 0, charArray, oldBase + l + n, + bottom - l - (n - 1)); + System.arraycopy(abuf, 0, charAttributes, oldBase + l + n, + bottom - l - (n - 1)); + cbuf = charArray; + abuf = charAttributes; + } else { + try { + if (n > (bottom - top) + 1) n = (bottom - top) + 1; + if (bufSize < maxBufSize) { + if (bufSize + n > maxBufSize) { + offset = n - (maxBufSize - bufSize); + scrollMarker += offset; + newBufSize = maxBufSize; + newScreenBase = maxBufSize - height - 1; + newWindowBase = screenBase; + } else { + scrollMarker += n; + newScreenBase += n; + newWindowBase += n; + newBufSize += n; + } + + cbuf = new char[newBufSize][]; + abuf = new int[newBufSize][]; + } else { + offset = n; + cbuf = charArray; + abuf = charAttributes; + } + // copy anything from the top of the buffer (+offset) to the new top + // up to the screenBase. + if (oldBase > 0) { + System.arraycopy(charArray, offset, + cbuf, 0, + oldBase - offset); + System.arraycopy(charAttributes, offset, + abuf, 0, + oldBase - offset); + } + // copy anything from the top of the screen (screenBase) up to the + // topMargin to the new screen + if (top > 0) { + System.arraycopy(charArray, oldBase, + cbuf, newScreenBase, + top); + System.arraycopy(charAttributes, oldBase, + abuf, newScreenBase, + top); + } + // copy anything from the topMargin up to the amount of lines inserted + // to the gap left over between scrollback buffer and screenBase + if (oldBase >= 0) { + System.arraycopy(charArray, oldBase + top, + cbuf, oldBase - offset, + n); + System.arraycopy(charAttributes, oldBase + top, + abuf, oldBase - offset, + n); + } + // copy anything from topMargin + n up to the line linserted to the + // topMargin + System.arraycopy(charArray, oldBase + top + n, + cbuf, newScreenBase + top, + l - top - (n - 1)); + System.arraycopy(charAttributes, oldBase + top + n, + abuf, newScreenBase + top, + l - top - (n - 1)); + // + // copy the all lines next to the inserted to the new buffer + if (l < height - 1) { + System.arraycopy(charArray, oldBase + l + 1, + cbuf, newScreenBase + l + 1, + (height - 1) - l); + System.arraycopy(charAttributes, oldBase + l + 1, + abuf, newScreenBase + l + 1, + (height - 1) - l); + } + } catch (ArrayIndexOutOfBoundsException e) { + // this should not happen anymore, but I will leave the code + // here in case something happens anyway. That code above is + // so complex I always have a hard time understanding what + // I did, even though there are comments + System.err.println("*** Error while scrolling up:"); + System.err.println("--- BEGIN STACK TRACE ---"); + e.printStackTrace(); + System.err.println("--- END STACK TRACE ---"); + System.err.println("bufSize=" + bufSize + ", maxBufSize=" + maxBufSize); + System.err.println("top=" + top + ", bottom=" + bottom); + System.err.println("n=" + n + ", l=" + l); + System.err.println("screenBase=" + screenBase + ", windowBase=" + windowBase); + System.err.println("newScreenBase=" + newScreenBase + ", newWindowBase=" + newWindowBase); + System.err.println("oldBase=" + oldBase); + System.err.println("size.width=" + width + ", size.height=" + height); + System.err.println("abuf.length=" + abuf.length + ", cbuf.length=" + cbuf.length); + System.err.println("*** done dumping debug information"); + } + } + + // this is a little helper to mark the scrolling + scrollMarker -= n; + + + for (int i = 0; i < n; i++) { + cbuf[(newScreenBase + l) + (scrollDown ? i : -i)] = new char[width]; + Arrays.fill(cbuf[(newScreenBase + l) + (scrollDown ? i : -i)], ' '); + abuf[(newScreenBase + l) + (scrollDown ? i : -i)] = new int[width]; + } + + charArray = cbuf; + charAttributes = abuf; + screenBase = newScreenBase; + windowBase = newWindowBase; + bufSize = newBufSize; + + if (scrollDown) + markLine(l, bottom - l + 1); + else + markLine(top, l - top + 1); + + display.updateScrollBar(); + } + + /** + * Delete a line at a specific position. Subsequent lines will be scrolled + * up to fill the space and a blank line is inserted at the end of the + * screen. + * @param l the y-coordinate to insert the line + * @see #deleteLine + */ + public void deleteLine(int l) { + int bottom = (l > bottomMargin ? height - 1: + (l < topMargin?topMargin:bottomMargin + 1)); + int numRows = bottom - l - 1; + + char[] discardedChars = charArray[screenBase + l]; + int[] discardedAttributes = charAttributes[screenBase + l]; + + if (numRows > 0) { + System.arraycopy(charArray, screenBase + l + 1, + charArray, screenBase + l, numRows); + System.arraycopy(charAttributes, screenBase + l + 1, + charAttributes, screenBase + l, numRows); + } + + int newBottomRow = screenBase + bottom - 1; + charArray[newBottomRow] = discardedChars; + charAttributes[newBottomRow] = discardedAttributes; + Arrays.fill(charArray[newBottomRow], ' '); + Arrays.fill(charAttributes[newBottomRow], 0); + + markLine(l, bottom - l); + } + + /** + * Delete a rectangular portion of the screen. + * You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (row) + * @param w with of the area in characters + * @param h height of the area in characters + * @param curAttr attribute to fill + * @see #deleteChar + * @see #deleteLine + * @see #redraw + */ + public void deleteArea(int c, int l, int w, int h, int curAttr) { + int endColumn = c + w; + int targetRow = screenBase + l; + for (int i = 0; i < h && l + i < height; i++) { + Arrays.fill(charAttributes[targetRow], c, endColumn, curAttr); + Arrays.fill(charArray[targetRow], c, endColumn, ' '); + targetRow++; + } + markLine(l, h); + } + + /** + * Delete a rectangular portion of the screen. + * You need to call redraw() to update the screen. + * @param c x-coordinate (column) + * @param l y-coordinate (row) + * @param w with of the area in characters + * @param h height of the area in characters + * @see #deleteChar + * @see #deleteLine + * @see #redraw + */ + public void deleteArea(int c, int l, int w, int h) { + deleteArea(c, l, w, h, 0); + } + + /** + * Sets whether the cursor is visible or not. + * @param doshow + */ + public void showCursor(boolean doshow) { + showcursor = doshow; + } + + /** + * Check whether the cursor is currently visible. + * @return visibility + */ + public boolean isCursorVisible() { + return showcursor; + } + + /** + * Puts the cursor at the specified position. + * @param c column + * @param l line + */ + public void setCursorPosition(int c, int l) { + cursorX = c; + cursorY = l; + } + + /** + * Get the current column of the cursor position. + */ + public int getCursorColumn() { + return cursorX; + } + + /** + * Get the current line of the cursor position. + */ + public int getCursorRow() { + return cursorY; + } + + /** + * Set the current window base. This allows to view the scrollback buffer. + * @param line the line where the screen window starts + * @see #setBufferSize + * @see #getBufferSize + */ + public void setWindowBase(int line) { + if (line > screenBase) + line = screenBase; + else if (line < 0) line = 0; + windowBase = line; + update[0] = true; + redraw(); + } + + /** + * Get the current window base. + * @see #setWindowBase + */ + public int getWindowBase() { + return windowBase; + } + + /** + * Set the scroll margins simultaneously. If they're out of bounds, trim them. + * @param l1 line that is the top + * @param l2 line that is the bottom + */ + public void setMargins(int l1, int l2) { + if (l1 > l2) + return; + + if (l1 < 0) + l1 = 0; + if (l2 >= height) + l2 = height - 1; + + topMargin = l1; + bottomMargin = l2; + } + + /** + * Set the top scroll margin for the screen. If the current bottom margin + * is smaller it will become the top margin and the line will become the + * bottom margin. + * @param l line that is the margin + */ + public void setTopMargin(int l) { + if (l > bottomMargin) { + topMargin = bottomMargin; + bottomMargin = l; + } else + topMargin = l; + if (topMargin < 0) topMargin = 0; + if (bottomMargin >= height) bottomMargin = height - 1; + } + + /** + * Get the top scroll margin. + */ + public int getTopMargin() { + return topMargin; + } + + /** + * Set the bottom scroll margin for the screen. If the current top margin + * is bigger it will become the bottom margin and the line will become the + * top margin. + * @param l line that is the margin + */ + public void setBottomMargin(int l) { + if (l < topMargin) { + bottomMargin = topMargin; + topMargin = l; + } else + bottomMargin = l; + if (topMargin < 0) topMargin = 0; + if (bottomMargin >= height) bottomMargin = height - 1; + } + + /** + * Get the bottom scroll margin. + */ + public int getBottomMargin() { + return bottomMargin; + } + + /** + * Set scrollback buffer size. + * @param amount new size of the buffer + */ + public void setBufferSize(int amount) { + if (amount < height) amount = height; + if (amount < maxBufSize) { + char cbuf[][] = new char[amount][width]; + int abuf[][] = new int[amount][width]; + int copyStart = bufSize - amount < 0 ? 0 : bufSize - amount; + int copyCount = bufSize - amount < 0 ? bufSize : amount; + if (charArray != null) + System.arraycopy(charArray, copyStart, cbuf, 0, copyCount); + if (charAttributes != null) + System.arraycopy(charAttributes, copyStart, abuf, 0, copyCount); + charArray = cbuf; + charAttributes = abuf; + bufSize = copyCount; + screenBase = bufSize - height; + windowBase = screenBase; + } + maxBufSize = amount; + + update[0] = true; + redraw(); + } + + /** + * Retrieve current scrollback buffer size. + * @see #setBufferSize + */ + public int getBufferSize() { + return bufSize; + } + + /** + * Retrieve maximum buffer Size. + * @see #getBufferSize + */ + public int getMaxBufferSize() { + return maxBufSize; + } + + /** + * Change the size of the screen. This will include adjustment of the + * scrollback buffer. + * @param w of the screen + * @param h of the screen + */ + public void setScreenSize(int w, int h, boolean broadcast) { + char cbuf[][]; + int abuf[][]; + int maxSize = bufSize; + + if (w < 1 || h < 1) return; + + if (debug > 0) + System.err.println("VDU: screen size [" + w + "," + h + "]"); + + if (h > maxBufSize) + maxBufSize = h; + + if (h > bufSize) { + bufSize = h; + screenBase = 0; + windowBase = 0; + } + + if (windowBase + h >= bufSize) + windowBase = bufSize - h; + + if (screenBase + h >= bufSize) + screenBase = bufSize - h; + + + cbuf = new char[bufSize][w]; + abuf = new int[bufSize][w]; + + + for (int i = 0; i < bufSize; i++) { + Arrays.fill(cbuf[i], ' '); + } + + if (bufSize < maxSize) + maxSize = bufSize; + + int rowLength; + if (charArray != null && charAttributes != null) { + for (int i = 0; i < maxSize && charArray[i] != null; i++) { + rowLength = charArray[i].length; + System.arraycopy(charArray[i], 0, cbuf[i], 0, + w < rowLength ? w : rowLength); + System.arraycopy(charAttributes[i], 0, abuf[i], 0, + w < rowLength ? w : rowLength); + } + } + + int C = getCursorColumn(); + if (C < 0) + C = 0; + else if (C >= width) + C = width - 1; + + int R = getCursorRow(); + if (R < 0) + R = 0; + else if (R >= height) + R = height - 1; + + setCursorPosition(C, R); + + charArray = cbuf; + charAttributes = abuf; + width = w; + height = h; + topMargin = 0; + bottomMargin = h - 1; + update = new boolean[h + 1]; + update[0] = true; + /* FIXME: ??? + if(resizeStrategy == RESIZE_FONT) + setBounds(getBounds()); + */ + } + + /** + * Get amount of rows on the screen. + */ + public int getRows() { + return height; + } + + /** + * Get amount of columns on the screen. + */ + public int getColumns() { + return width; + } + + /** + * Mark lines to be updated with redraw(). + * @param l starting line + * @param n amount of lines to be updated + * @see #redraw + */ + public void markLine(int l, int n) { + for (int i = 0; (i < n) && (l + i < height); i++) + update[l + i + 1] = true; + } + +// private static int checkBounds(int value, int lower, int upper) { +// if (value < lower) +// return lower; +// else if (value > upper) +// return upper; +// else +// return value; +// } + + /** a generic display that should redraw on demand */ + protected VDUDisplay display; + + public void setDisplay(VDUDisplay display) { + this.display = display; + } + + /** + * Trigger a redraw on the display. + */ + protected void redraw() { + if (display != null) + display.redraw(); + } +} diff --git a/app/src/main/java/de/mud/terminal/VDUDisplay.java b/app/src/main/java/de/mud/terminal/VDUDisplay.java new file mode 100644 index 0000000..bb4a7b8 --- /dev/null +++ b/app/src/main/java/de/mud/terminal/VDUDisplay.java @@ -0,0 +1,40 @@ +/* + * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". + * + * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved. + * + * Please visit http://javatelnet.org/ for updates and contact. + * + * --LICENSE NOTICE-- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + * --LICENSE NOTICE-- + * + */ + +package de.mud.terminal; + +/** + * Generic display + */ +public interface VDUDisplay { + public void redraw(); + public void updateScrollBar(); + + public void setVDUBuffer(VDUBuffer buffer); + public VDUBuffer getVDUBuffer(); + + public void setColor(int index, int red, int green, int blue); + public void resetColors(); +} diff --git a/app/src/main/java/de/mud/terminal/VDUInput.java b/app/src/main/java/de/mud/terminal/VDUInput.java new file mode 100644 index 0000000..43c88de --- /dev/null +++ b/app/src/main/java/de/mud/terminal/VDUInput.java @@ -0,0 +1,90 @@ +/* + * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". + * + * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved. + * + * Please visit http://javatelnet.org/ for updates and contact. + * + * --LICENSE NOTICE-- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + * --LICENSE NOTICE-- + * + */ +package de.mud.terminal; + +import java.util.Properties; + +/** + * An interface for a terminal that accepts input from keyboard and mouse. + * + * @author Matthias L. Jugel, Marcus Meißner + * @version $Id: VDUInput.java 499 2005-09-29 08:24:54Z leo $ + */ +public interface VDUInput { + + public final static int KEY_CONTROL = 0x01; + public final static int KEY_SHIFT = 0x02; + public final static int KEY_ALT = 0x04; + public final static int KEY_ACTION = 0x08; + + + + /** + * Direct access to writing data ... + * @param b + */ + void write(byte b[]); + + /** + * Terminal is mouse-aware and requires (x,y) coordinates of + * on the terminal (character coordinates) and the button clicked. + * @param x + * @param y + * @param modifiers + */ + void mousePressed(int x, int y, int modifiers); + + /** + * Terminal is mouse-aware and requires the coordinates and button + * of the release. + * @param x + * @param y + * @param modifiers + */ + void mouseReleased(int x, int y, int modifiers); + + /** + * Override the standard key codes used by the terminal emulation. + * @param codes a properties object containing key code definitions + */ + void setKeyCodes(Properties codes); + + /** + * main keytyping event handler... + * @param keyCode the key code + * @param keyChar the character represented by the key + * @param modifiers shift/alt/control modifiers + */ + void keyPressed(int keyCode, char keyChar, int modifiers); + + /** + * Handle key Typed events for the terminal, this will get + * all normal key types, but no shift/alt/control/numlock. + * @param keyCode the key code + * @param keyChar the character represented by the key + * @param modifiers shift/alt/control modifiers + */ + void keyTyped(int keyCode, char keyChar, int modifiers); +} diff --git a/app/src/main/java/de/mud/terminal/vt320.java b/app/src/main/java/de/mud/terminal/vt320.java new file mode 100644 index 0000000..73369af --- /dev/null +++ b/app/src/main/java/de/mud/terminal/vt320.java @@ -0,0 +1,3032 @@ +/* + * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform". + * + * (c) Matthias L. Jugel, Marcus Meiner 1996-2005. All Rights Reserved. + * + * Please visit http://javatelnet.org/ for updates and contact. + * + * --LICENSE NOTICE-- + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + * --LICENSE NOTICE-- + * + */ + +package de.mud.terminal; + +import android.text.AndroidCharacter; + +import java.util.Properties; + +/** + * Implementation of a VT terminal emulation plus ANSI compatible. + *

+ * Maintainer: Marcus Meißner + * + * @version $Id: vt320.java 507 2005-10-25 10:14:52Z marcus $ + * @author Matthias L. Jugel, Marcus Meißner + */ +public abstract class vt320 extends VDUBuffer implements VDUInput { + + /** The current version id tag.

+ * $Id: vt320.java 507 2005-10-25 10:14:52Z marcus $ + * + */ + public final static String ID = "$Id: vt320.java 507 2005-10-25 10:14:52Z marcus $"; + + /** the debug level */ + private final static int debug = 0; + private StringBuilder debugStr; + public abstract void debug(String notice); + + /** + * Write an answer back to the remote host. This is needed to be able to + * send terminal answers requests like status and type information. + * @param b the array of bytes to be sent + */ + public abstract void write(byte[] b); + + /** + * Write an answer back to the remote host. This is needed to be able to + * send terminal answers requests like status and type information. + * @param b the array of bytes to be sent + */ + public abstract void write(int b); + + /** + * Play the beep sound ... + */ + public void beep() { /* do nothing by default */ + } + + /** + * Convenience function for putString(char[], int, int) + */ + public void putString(String s) { + int len = s.length(); + char[] tmp = new char[len]; + s.getChars(0, len, tmp, 0); + putString(tmp, null, 0, len); + } + + /** + * Put string at current cursor position. Moves cursor + * according to the String. Does NOT wrap. + * @param s character array + * @param start place to start in array + * @param len number of characters to process + */ + public void putString(char[] s, byte[] fullwidths, int start, int len) { + if (len > 0) { + //markLine(R, 1); + int lastChar = -1; + char c; + boolean isWide = false; + + for (int i = 0; i < len; i++) { + c = s[start + i]; + // Shortcut for my favorite ASCII + if (c <= 0x7F) { + if (lastChar != -1) + putChar((char) lastChar, isWide, false); + lastChar = c; + isWide = false; + } else if (!Character.isLowSurrogate(c) && !Character.isHighSurrogate(c)) { + if (Character.getType(c) == Character.NON_SPACING_MARK) { + if (lastChar != -1) { + char nc = Precomposer.precompose((char) lastChar, c); + putChar(nc, isWide, false); + lastChar = -1; + } + } else { + if (lastChar != -1) + putChar((char) lastChar, isWide, false); + lastChar = c; + if (fullwidths != null) { + final byte width = fullwidths[i]; + isWide = (width == AndroidCharacter.EAST_ASIAN_WIDTH_WIDE) + || (width == AndroidCharacter.EAST_ASIAN_WIDTH_FULL_WIDTH); + } + } + } + } + + if (lastChar != -1) + putChar((char) lastChar, isWide, false); + + setCursorPosition(C, R); + redraw(); + } + } + + protected void sendTelnetCommand(byte cmd) { + + } + + /** + * Sent the changed window size from the terminal to all listeners. + */ + protected void setWindowSize(int c, int r) { + /* To be overridden by Terminal.java */ + } + + @Override +public void setScreenSize(int c, int r, boolean broadcast) { + int oldrows = height; + + if (debug>2) { + if (debugStr == null) + debugStr = new StringBuilder(); + + debugStr.append("setscreensize (") + .append(c) + .append(',') + .append(r) + .append(',') + .append(broadcast) + .append(')'); + debug(debugStr.toString()); + debugStr.setLength(0); + } + + super.setScreenSize(c,r,false); + + boolean cursorChanged = false; + + // Don't let the cursor go off the screen. + if (C >= c) { + C = c - 1; + cursorChanged = true; + } + + if (R >= r) { + R = r - 1; + cursorChanged = true; + } + + if (cursorChanged) { + setCursorPosition(C, R); + redraw(); + } + + if (broadcast) { + setWindowSize(c, r); /* broadcast up */ + } + } + + + /** + * Create a new vt320 terminal and intialize it with useful settings. + */ + public vt320(int width, int height) { + super(width, height); + + debugStr = new StringBuilder(); + + setVMS(false); + setIBMCharset(false); + setTerminalID("vt320"); + setBufferSize(100); + //setBorder(2, false); + + gx = new char[4]; + reset(); + + /* top row of numpad */ + PF1 = "\u001bOP"; + PF2 = "\u001bOQ"; + PF3 = "\u001bOR"; + PF4 = "\u001bOS"; + + /* the 3x2 keyblock on PC keyboards */ + Insert = new String[4]; + Remove = new String[4]; + KeyHome = new String[4]; + KeyEnd = new String[4]; + NextScn = new String[4]; + PrevScn = new String[4]; + Escape = new String[4]; + BackSpace = new String[4]; + TabKey = new String[4]; + Insert[0] = Insert[1] = Insert[2] = Insert[3] = "\u001b[2~"; + Remove[0] = Remove[1] = Remove[2] = Remove[3] = "\u001b[3~"; + PrevScn[0] = PrevScn[1] = PrevScn[2] = PrevScn[3] = "\u001b[5~"; + NextScn[0] = NextScn[1] = NextScn[2] = NextScn[3] = "\u001b[6~"; + KeyHome[0] = KeyHome[1] = KeyHome[2] = KeyHome[3] = "\u001b[H"; + KeyEnd[0] = KeyEnd[1] = KeyEnd[2] = KeyEnd[3] = "\u001b[F"; + Escape[0] = Escape[1] = Escape[2] = Escape[3] = "\u001b"; + if (vms) { + BackSpace[1] = "" + (char) 10; // VMS shift deletes word back + BackSpace[2] = "\u0018"; // VMS control deletes line back + BackSpace[0] = BackSpace[3] = "\u007f"; // VMS other is delete + } else { + //BackSpace[0] = BackSpace[1] = BackSpace[2] = BackSpace[3] = "\b"; + // ConnectBot modifications. + BackSpace[0] = "\b"; + BackSpace[1] = "\u007f"; + BackSpace[2] = "\u001b[3~"; + BackSpace[3] = "\u001b[2~"; + } + + /* some more VT100 keys */ + Find = "\u001b[1~"; + Select = "\u001b[4~"; + Help = "\u001b[28~"; + Do = "\u001b[29~"; + + FunctionKey = new String[21]; + FunctionKey[0] = ""; + FunctionKey[1] = PF1; + FunctionKey[2] = PF2; + FunctionKey[3] = PF3; + FunctionKey[4] = PF4; + /* following are defined differently for vt220 / vt132 ... */ + FunctionKey[5] = "\u001b[15~"; + FunctionKey[6] = "\u001b[17~"; + FunctionKey[7] = "\u001b[18~"; + FunctionKey[8] = "\u001b[19~"; + FunctionKey[9] = "\u001b[20~"; + FunctionKey[10] = "\u001b[21~"; + FunctionKey[11] = "\u001b[23~"; + FunctionKey[12] = "\u001b[24~"; + FunctionKey[13] = "\u001b[25~"; + FunctionKey[14] = "\u001b[26~"; + FunctionKey[15] = Help; + FunctionKey[16] = Do; + FunctionKey[17] = "\u001b[31~"; + FunctionKey[18] = "\u001b[32~"; + FunctionKey[19] = "\u001b[33~"; + FunctionKey[20] = "\u001b[34~"; + + FunctionKeyShift = new String[21]; + FunctionKeyAlt = new String[21]; + FunctionKeyCtrl = new String[21]; + + for (int i = 0; i < 20; i++) { + FunctionKeyShift[i] = ""; + FunctionKeyAlt[i] = ""; + FunctionKeyCtrl[i] = ""; + } + FunctionKeyShift[15] = Find; + FunctionKeyShift[16] = Select; + + + TabKey[0] = "\u0009"; + TabKey[1] = "\u001bOP\u0009"; + TabKey[2] = TabKey[3] = ""; + + KeyUp = new String[4]; + KeyUp[0] = "\u001b[A"; + KeyDown = new String[4]; + KeyDown[0] = "\u001b[B"; + KeyRight = new String[4]; + KeyRight[0] = "\u001b[C"; + KeyLeft = new String[4]; + KeyLeft[0] = "\u001b[D"; + Numpad = new String[10]; + Numpad[0] = "\u001bOp"; + Numpad[1] = "\u001bOq"; + Numpad[2] = "\u001bOr"; + Numpad[3] = "\u001bOs"; + Numpad[4] = "\u001bOt"; + Numpad[5] = "\u001bOu"; + Numpad[6] = "\u001bOv"; + Numpad[7] = "\u001bOw"; + Numpad[8] = "\u001bOx"; + Numpad[9] = "\u001bOy"; + KPMinus = PF4; + KPComma = "\u001bOl"; + KPPeriod = "\u001bOn"; + KPEnter = "\u001bOM"; + + NUMPlus = new String[4]; + NUMPlus[0] = "+"; + NUMDot = new String[4]; + NUMDot[0] = "."; + } + + public void setBackspace(int type) { + switch (type) { + case DELETE_IS_DEL: + BackSpace[0] = "\u007f"; + BackSpace[1] = "\b"; + break; + case DELETE_IS_BACKSPACE: + BackSpace[0] = "\b"; + BackSpace[1] = "\u007f"; + break; + } + } + + /** + * Create a default vt320 terminal with 80 columns and 24 lines. + */ + public vt320() { + this(80, 24); + } + + /** + * Terminal is mouse-aware and requires (x,y) coordinates of + * on the terminal (character coordinates) and the button clicked. + * @param x + * @param y + * @param modifiers + */ + public void mousePressed(int x, int y, int modifiers) { + if (mouserpt == 0) + return; + + int mods = modifiers; + mousebut = 3; + if ((mods & 16) == 16) mousebut = 0; + if ((mods & 8) == 8) mousebut = 1; + if ((mods & 4) == 4) mousebut = 2; + + int mousecode; + if (mouserpt == 9) /* X10 Mouse */ + mousecode = 0x20 | mousebut; + else /* normal xterm mouse reporting */ + mousecode = mousebut | 0x20 | ((mods & 7) << 2); + + byte b[] = new byte[6]; + + b[0] = 27; + b[1] = (byte) '['; + b[2] = (byte) 'M'; + b[3] = (byte) mousecode; + b[4] = (byte) (0x20 + x + 1); + b[5] = (byte) (0x20 + y + 1); + + write(b); // FIXME: writeSpecial here + } + + /** + * Terminal is mouse-aware and requires the coordinates and button + * of the release. + * @param x + * @param y + * @param modifiers + */ + public void mouseReleased(int x, int y, int modifiers) { + if (mouserpt == 0) + return; + + /* problem is tht modifiers still have the released button set in them. + int mods = modifiers; + mousebut = 3; + if ((mods & 16)==16) mousebut=0; + if ((mods & 8)==8 ) mousebut=1; + if ((mods & 4)==4 ) mousebut=2; + */ + + int mousecode; + if (mouserpt == 9) + mousecode = 0x20 + mousebut; /* same as press? appears so. */ + else + mousecode = '#'; + + byte b[] = new byte[6]; + b[0] = 27; + b[1] = (byte) '['; + b[2] = (byte) 'M'; + b[3] = (byte) mousecode; + b[4] = (byte) (0x20 + x + 1); + b[5] = (byte) (0x20 + y + 1); + write(b); // FIXME: writeSpecial here + mousebut = 0; + } + + + /** we should do localecho (passed from other modules). false is default */ + private boolean localecho = false; + + /** + * Enable or disable the local echo property of the terminal. + * @param echo true if the terminal should echo locally + */ + public void setLocalEcho(boolean echo) { + localecho = echo; + } + + /** + * Enable the VMS mode of the terminal to handle some things differently + * for VMS hosts. + * @param vms true for vms mode, false for normal mode + */ + public void setVMS(boolean vms) { + this.vms = vms; + } + + /** + * Enable the usage of the IBM character set used by some BBS's. Special + * graphical character are available in this mode. + * @param ibm true to use the ibm character set + */ + public void setIBMCharset(boolean ibm) { + useibmcharset = ibm; + } + + /** + * Override the standard key codes used by the terminal emulation. + * @param codes a properties object containing key code definitions + */ + public void setKeyCodes(Properties codes) { + String res, prefixes[] = {"", "S", "C", "A"}; + int i; + + for (i = 0; i < 10; i++) { + res = codes.getProperty("NUMPAD" + i); + if (res != null) Numpad[i] = unEscape(res); + } + for (i = 1; i < 20; i++) { + res = codes.getProperty("F" + i); + if (res != null) FunctionKey[i] = unEscape(res); + res = codes.getProperty("SF" + i); + if (res != null) FunctionKeyShift[i] = unEscape(res); + res = codes.getProperty("CF" + i); + if (res != null) FunctionKeyCtrl[i] = unEscape(res); + res = codes.getProperty("AF" + i); + if (res != null) FunctionKeyAlt[i] = unEscape(res); + } + for (i = 0; i < 4; i++) { + res = codes.getProperty(prefixes[i] + "PGUP"); + if (res != null) PrevScn[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "PGDOWN"); + if (res != null) NextScn[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "END"); + if (res != null) KeyEnd[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "HOME"); + if (res != null) KeyHome[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "INSERT"); + if (res != null) Insert[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "REMOVE"); + if (res != null) Remove[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "UP"); + if (res != null) KeyUp[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "DOWN"); + if (res != null) KeyDown[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "LEFT"); + if (res != null) KeyLeft[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "RIGHT"); + if (res != null) KeyRight[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "ESCAPE"); + if (res != null) Escape[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "BACKSPACE"); + if (res != null) BackSpace[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "TAB"); + if (res != null) TabKey[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "NUMPLUS"); + if (res != null) NUMPlus[i] = unEscape(res); + res = codes.getProperty(prefixes[i] + "NUMDECIMAL"); + if (res != null) NUMDot[i] = unEscape(res); + } + } + + /** + * Set the terminal id used to identify this terminal. + * @param terminalID the id string + */ + public void setTerminalID(String terminalID) { + this.terminalID = terminalID; + + if (terminalID.equals("scoansi")) { + FunctionKey[1] = "\u001b[M"; FunctionKey[2] = "\u001b[N"; + FunctionKey[3] = "\u001b[O"; FunctionKey[4] = "\u001b[P"; + FunctionKey[5] = "\u001b[Q"; FunctionKey[6] = "\u001b[R"; + FunctionKey[7] = "\u001b[S"; FunctionKey[8] = "\u001b[T"; + FunctionKey[9] = "\u001b[U"; FunctionKey[10] = "\u001b[V"; + FunctionKey[11] = "\u001b[W"; FunctionKey[12] = "\u001b[X"; + FunctionKey[13] = "\u001b[Y"; FunctionKey[14] = "?"; + FunctionKey[15] = "\u001b[a"; FunctionKey[16] = "\u001b[b"; + FunctionKey[17] = "\u001b[c"; FunctionKey[18] = "\u001b[d"; + FunctionKey[19] = "\u001b[e"; FunctionKey[20] = "\u001b[f"; + PrevScn[0] = PrevScn[1] = PrevScn[2] = PrevScn[3] = "\u001b[I"; + NextScn[0] = NextScn[1] = NextScn[2] = NextScn[3] = "\u001b[G"; + // more theoretically. + } + } + + public void setAnswerBack(String ab) { + this.answerBack = unEscape(ab); + } + + /** + * Get the terminal id used to identify this terminal. + */ + public String getTerminalID() { + return terminalID; + } + + /** + * A small conveniance method thar converts the string to a byte array + * for sending. + * @param s the string to be sent + */ + private boolean write(String s, boolean doecho) { + if (debug > 2) { + debugStr.append("write(|") + .append(s) + .append("|,") + .append(doecho); + debug(debugStr.toString()); + debugStr.setLength(0); + } + if (s == null) // aka the empty string. + return true; + /* NOTE: getBytes() honours some locale, it *CONVERTS* the string. + * However, we output only 7bit stuff towards the target, and *some* + * 8 bit control codes. We must not mess up the latter, so we do hand + * by hand copy. + */ + + byte arr[] = new byte[s.length()]; + for (int i = 0; i < s.length(); i++) { + arr[i] = (byte) s.charAt(i); + } + write(arr); + + if (doecho) + putString(s); + return true; + } + + private boolean write(int s, boolean doecho) { + if (debug > 2) { + debugStr.append("write(|") + .append(s) + .append("|,") + .append(doecho); + debug(debugStr.toString()); + debugStr.setLength(0); + } + + write(s); + + // TODO check if character is wide + if (doecho) + putChar((char)s, false, false); + return true; + } + + private boolean write(String s) { + return write(s, localecho); + } + + // =================================================================== + // the actual terminal emulation code comes here: + // =================================================================== + + private String terminalID = "vt320"; + private String answerBack = "Use Terminal.answerback to set ...\n"; + + // X - COLUMNS, Y - ROWS + int R,C; + int attributes = 0; + + int Sc,Sr,Sa,Stm,Sbm; + char Sgr,Sgl; + char Sgx[]; + + int insertmode = 0; + int statusmode = 0; + boolean vt52mode = false; + boolean keypadmode = false; /* false - numeric, true - application */ + boolean output8bit = false; + int normalcursor = 0; + boolean moveoutsidemargins = true; + boolean wraparound = true; + boolean sendcrlf = true; + boolean capslock = false; + boolean numlock = false; + int mouserpt = 0; + byte mousebut = 0; + + boolean useibmcharset = false; + + int lastwaslf = 0; + boolean usedcharsets = false; + + private final static char ESC = 27; + private final static char IND = 132; + private final static char NEL = 133; + private final static char RI = 141; + private final static char SS2 = 142; + private final static char SS3 = 143; + private final static char DCS = 144; + private final static char HTS = 136; + private final static char CSI = 155; + private final static char OSC = 157; + private final static int TSTATE_DATA = 0; + private final static int TSTATE_ESC = 1; /* ESC */ + private final static int TSTATE_CSI = 2; /* ESC [ */ + private final static int TSTATE_DCS = 3; /* ESC P */ + private final static int TSTATE_DCEQ = 4; /* ESC [? */ + private final static int TSTATE_ESCSQUARE = 5; /* ESC # */ + private final static int TSTATE_OSC = 6; /* ESC ] */ + private final static int TSTATE_SETG0 = 7; /* ESC (? */ + private final static int TSTATE_SETG1 = 8; /* ESC )? */ + private final static int TSTATE_SETG2 = 9; /* ESC *? */ + private final static int TSTATE_SETG3 = 10; /* ESC +? */ + private final static int TSTATE_CSI_DOLLAR = 11; /* ESC [ Pn $ */ + private final static int TSTATE_CSI_EX = 12; /* ESC [ ! */ + private final static int TSTATE_ESCSPACE = 13; /* ESC */ + private final static int TSTATE_VT52X = 14; + private final static int TSTATE_VT52Y = 15; + private final static int TSTATE_CSI_TICKS = 16; + private final static int TSTATE_CSI_EQUAL = 17; /* ESC [ = */ + private final static int TSTATE_TITLE = 18; /* xterm title */ + + /* Keys we support */ + public final static int KEY_PAUSE = 1; + public final static int KEY_F1 = 2; + public final static int KEY_F2 = 3; + public final static int KEY_F3 = 4; + public final static int KEY_F4 = 5; + public final static int KEY_F5 = 6; + public final static int KEY_F6 = 7; + public final static int KEY_F7 = 8; + public final static int KEY_F8 = 9; + public final static int KEY_F9 = 10; + public final static int KEY_F10 = 11; + public final static int KEY_F11 = 12; + public final static int KEY_F12 = 13; + public final static int KEY_UP = 14; + public final static int KEY_DOWN =15 ; + public final static int KEY_LEFT = 16; + public final static int KEY_RIGHT = 17; + public final static int KEY_PAGE_DOWN = 18; + public final static int KEY_PAGE_UP = 19; + public final static int KEY_INSERT = 20; + public final static int KEY_DELETE = 21; + public final static int KEY_BACK_SPACE = 22; + public final static int KEY_HOME = 23; + public final static int KEY_END = 24; + public final static int KEY_NUM_LOCK = 25; + public final static int KEY_CAPS_LOCK = 26; + public final static int KEY_SHIFT = 27; + public final static int KEY_CONTROL = 28; + public final static int KEY_ALT = 29; + public final static int KEY_ENTER = 30; + public final static int KEY_NUMPAD0 = 31; + public final static int KEY_NUMPAD1 = 32; + public final static int KEY_NUMPAD2 = 33; + public final static int KEY_NUMPAD3 = 34; + public final static int KEY_NUMPAD4 = 35; + public final static int KEY_NUMPAD5 = 36; + public final static int KEY_NUMPAD6 = 37; + public final static int KEY_NUMPAD7 = 38; + public final static int KEY_NUMPAD8 = 39; + public final static int KEY_NUMPAD9 = 40; + public final static int KEY_DECIMAL = 41; + public final static int KEY_ADD = 42; + public final static int KEY_ESCAPE = 43; + + public final static int DELETE_IS_DEL = 0; + public final static int DELETE_IS_BACKSPACE = 1; + + /* The graphics charsets + * B - default ASCII + * A - ISO Latin 1 + * 0 - DEC SPECIAL + * < - User defined + * .... + */ + char gx[]; + char gl; // GL (left charset) + char gr; // GR (right charset) + int onegl; // single shift override for GL. + + // Map from scoansi linedrawing to DEC _and_ unicode (for the stuff which + // is not in linedrawing). Got from experimenting with scoadmin. + private final static String scoansi_acs = "Tm7k3x4u?kZl@mYjEnB\u2566DqCtAvM\u2550:\u2551N\u2557I\u2554;\u2557H\u255a0a<\u255d"; + // array to store DEC Special -> Unicode mapping + // Unicode DEC Unicode name (DEC name) + private static char DECSPECIAL[] = { + '\u0040', //5f blank + '\u2666', //60 black diamond + '\u2592', //61 grey square + '\u2409', //62 Horizontal tab (ht) pict. for control + '\u240c', //63 Form Feed (ff) pict. for control + '\u240d', //64 Carriage Return (cr) pict. for control + '\u240a', //65 Line Feed (lf) pict. for control + '\u00ba', //66 Masculine ordinal indicator + '\u00b1', //67 Plus or minus sign + '\u2424', //68 New Line (nl) pict. for control + '\u240b', //69 Vertical Tab (vt) pict. for control + '\u2518', //6a Forms light up and left + '\u2510', //6b Forms light down and left + '\u250c', //6c Forms light down and right + '\u2514', //6d Forms light up and right + '\u253c', //6e Forms light vertical and horizontal + '\u2594', //6f Upper 1/8 block (Scan 1) + '\u2580', //70 Upper 1/2 block (Scan 3) + '\u2500', //71 Forms light horizontal or ?em dash? (Scan 5) + '\u25ac', //72 \u25ac black rect. or \u2582 lower 1/4 (Scan 7) + '\u005f', //73 \u005f underscore or \u2581 lower 1/8 (Scan 9) + '\u251c', //74 Forms light vertical and right + '\u2524', //75 Forms light vertical and left + '\u2534', //76 Forms light up and horizontal + '\u252c', //77 Forms light down and horizontal + '\u2502', //78 vertical bar + '\u2264', //79 less than or equal + '\u2265', //7a greater than or equal + '\u00b6', //7b paragraph + '\u2260', //7c not equal + '\u00a3', //7d Pound Sign (british) + '\u00b7' //7e Middle Dot + }; + + /** Strings to send on function key pressing */ + private String Numpad[]; + private String FunctionKey[]; + private String FunctionKeyShift[]; + private String FunctionKeyCtrl[]; + private String FunctionKeyAlt[]; + private String TabKey[]; + private String KeyUp[],KeyDown[],KeyLeft[],KeyRight[]; + private String KPMinus, KPComma, KPPeriod, KPEnter; + private String PF1, PF2, PF3, PF4; + private String Help, Do, Find, Select; + + private String KeyHome[], KeyEnd[], Insert[], Remove[], PrevScn[], NextScn[]; + private String Escape[], BackSpace[], NUMDot[], NUMPlus[]; + + private String osc,dcs; /* to memorize OSC & DCS control sequence */ + + /** vt320 state variable (internal) */ + private int term_state = TSTATE_DATA; + /** in vms mode, set by Terminal.VMS property */ + private boolean vms = false; + /** Tabulators */ + private byte[] Tabs; + /** The list of integers as used by CSI */ + private int[] DCEvars = new int[30]; + private int DCEvar; + + /** + * Replace escape code characters (backslash + identifier) with their + * respective codes. + * @param tmp the string to be parsed + * @return a unescaped string + */ + static String unEscape(String tmp) { + int idx = 0, oldidx = 0; + String cmd; + // f.println("unescape("+tmp+")"); + cmd = ""; + while ((idx = tmp.indexOf('\\', oldidx)) >= 0 && + ++idx <= tmp.length()) { + cmd += tmp.substring(oldidx, idx - 1); + if (idx == tmp.length()) return cmd; + switch (tmp.charAt(idx)) { + case 'b': + cmd += "\b"; + break; + case 'e': + cmd += "\u001b"; + break; + case 'n': + cmd += "\n"; + break; + case 'r': + cmd += "\r"; + break; + case 't': + cmd += "\t"; + break; + case 'v': + cmd += "\u000b"; + break; + case 'a': + cmd += "\u0012"; + break; + default : + if ((tmp.charAt(idx) >= '0') && (tmp.charAt(idx) <= '9')) { + int i; + for (i = idx; i < tmp.length(); i++) + if ((tmp.charAt(i) < '0') || (tmp.charAt(i) > '9')) + break; + cmd += (char) Integer.parseInt(tmp.substring(idx, i)); + idx = i - 1; + } else + cmd += tmp.substring(idx, ++idx); + break; + } + oldidx = ++idx; + } + if (oldidx <= tmp.length()) cmd += tmp.substring(oldidx); + return cmd; + } + + /** + * A small conveniance method thar converts a 7bit string to the 8bit + * version depending on VT52/Output8Bit mode. + * + * @param s the string to be sent + */ + private boolean writeSpecial(String s) { + if (s == null) + return true; + if (((s.length() >= 3) && (s.charAt(0) == 27) && (s.charAt(1) == 'O'))) { + if (vt52mode) { + if ((s.charAt(2) >= 'P') && (s.charAt(2) <= 'S')) { + s = "\u001b" + s.substring(2); /* ESC x */ + } else { + s = "\u001b?" + s.substring(2); /* ESC ? x */ + } + } else { + if (output8bit) { + s = "\u008f" + s.substring(2); /* SS3 x */ + } /* else keep string as it is */ + } + } + if (((s.length() >= 3) && (s.charAt(0) == 27) && (s.charAt(1) == '['))) { + if (output8bit) { + s = "\u009b" + s.substring(2); /* CSI ... */ + } /* else keep */ + } + return write(s, false); + } + + /** + * main keytyping event handler... + */ + public void keyPressed(int keyCode, char keyChar, int modifiers) { + boolean control = (modifiers & VDUInput.KEY_CONTROL) != 0; + boolean shift = (modifiers & VDUInput.KEY_SHIFT) != 0; + boolean alt = (modifiers & VDUInput.KEY_ALT) != 0; + + if (debug > 1) { + debugStr.append("keyPressed(") + .append(keyCode) + .append(", ") + .append((int)keyChar) + .append(", ") + .append(modifiers) + .append(')'); + debug(debugStr.toString()); + debugStr.setLength(0); + } + + int xind; + String fmap[]; + xind = 0; + fmap = FunctionKey; + if (shift) { + fmap = FunctionKeyShift; + xind = 1; + } + if (control) { + fmap = FunctionKeyCtrl; + xind = 2; + } + if (alt) { + fmap = FunctionKeyAlt; + xind = 3; + } + + switch (keyCode) { + case KEY_PAUSE: + if (shift || control) + sendTelnetCommand((byte) 243); // BREAK + break; + case KEY_F1: + writeSpecial(fmap[1]); + break; + case KEY_F2: + writeSpecial(fmap[2]); + break; + case KEY_F3: + writeSpecial(fmap[3]); + break; + case KEY_F4: + writeSpecial(fmap[4]); + break; + case KEY_F5: + writeSpecial(fmap[5]); + break; + case KEY_F6: + writeSpecial(fmap[6]); + break; + case KEY_F7: + writeSpecial(fmap[7]); + break; + case KEY_F8: + writeSpecial(fmap[8]); + break; + case KEY_F9: + writeSpecial(fmap[9]); + break; + case KEY_F10: + writeSpecial(fmap[10]); + break; + case KEY_F11: + writeSpecial(fmap[11]); + break; + case KEY_F12: + writeSpecial(fmap[12]); + break; + case KEY_UP: + writeSpecial(KeyUp[xind]); + break; + case KEY_DOWN: + writeSpecial(KeyDown[xind]); + break; + case KEY_LEFT: + writeSpecial(KeyLeft[xind]); + break; + case KEY_RIGHT: + writeSpecial(KeyRight[xind]); + break; + case KEY_PAGE_DOWN: + writeSpecial(NextScn[xind]); + break; + case KEY_PAGE_UP: + writeSpecial(PrevScn[xind]); + break; + case KEY_INSERT: + writeSpecial(Insert[xind]); + break; + case KEY_DELETE: + writeSpecial(Remove[xind]); + break; + case KEY_BACK_SPACE: + writeSpecial(BackSpace[xind]); + if (localecho) { + if (BackSpace[xind] == "\b") { + putString("\b \b"); // make the last char 'deleted' + } else { + putString(BackSpace[xind]); // echo it + } + } + break; + case KEY_HOME: + writeSpecial(KeyHome[xind]); + break; + case KEY_END: + writeSpecial(KeyEnd[xind]); + break; + case KEY_NUM_LOCK: + if (vms && control) { + writeSpecial(PF1); + } + if (!control) + numlock = !numlock; + break; + case KEY_CAPS_LOCK: + capslock = !capslock; + return; + case KEY_SHIFT: + case KEY_CONTROL: + case KEY_ALT: + return; + default: + break; + } + } +/* + public void keyReleased(KeyEvent evt) { + if (debug > 1) debug("keyReleased("+evt+")"); + // ignore + } +*/ + /** + * Handle key Typed events for the terminal, this will get + * all normal key types, but no shift/alt/control/numlock. + */ + public void keyTyped(int keyCode, char keyChar, int modifiers) { + boolean control = (modifiers & VDUInput.KEY_CONTROL) != 0; + boolean shift = (modifiers & VDUInput.KEY_SHIFT) != 0; + boolean alt = (modifiers & VDUInput.KEY_ALT) != 0; + + if (debug > 1) debug("keyTyped("+keyCode+", "+(int)keyChar+", "+modifiers+")"); + + if (keyChar == '\t') { + if (shift) { + write(TabKey[1], false); + } else { + if (control) { + write(TabKey[2], false); + } else { + if (alt) { + write(TabKey[3], false); + } else { + write(TabKey[0], false); + } + } + } + return; + } + if (alt) { + write(((char) (keyChar | 0x80))); + return; + } + + if (((keyCode == KEY_ENTER) || (keyChar == 10)) + && !control) { + write('\r'); + if (localecho) putString("\r\n"); // bad hack + return; + } + + if ((keyCode == 10) && !control) { + debug("Sending \\r"); + write('\r'); + return; + } + + // FIXME: on german PC keyboards you have to use Alt-Ctrl-q to get an @, + // so we can't just use it here... will probably break some other VMS + // codes. -Marcus + // if(((!vms && keyChar == '2') || keyChar == '@' || keyChar == ' ') + // && control) + if (((!vms && keyChar == '2') || keyChar == ' ') && control) + write(0); + + if (vms) { + if (keyChar == 127 && !control) { + if (shift) + writeSpecial(Insert[0]); // VMS shift delete = insert + else + writeSpecial(Remove[0]); // VMS delete = remove + return; + } else if (control) + switch (keyChar) { + case '0': + writeSpecial(Numpad[0]); + return; + case '1': + writeSpecial(Numpad[1]); + return; + case '2': + writeSpecial(Numpad[2]); + return; + case '3': + writeSpecial(Numpad[3]); + return; + case '4': + writeSpecial(Numpad[4]); + return; + case '5': + writeSpecial(Numpad[5]); + return; + case '6': + writeSpecial(Numpad[6]); + return; + case '7': + writeSpecial(Numpad[7]); + return; + case '8': + writeSpecial(Numpad[8]); + return; + case '9': + writeSpecial(Numpad[9]); + return; + case '.': + writeSpecial(KPPeriod); + return; + case '-': + case 31: + writeSpecial(KPMinus); + return; + case '+': + writeSpecial(KPComma); + return; + case 10: + writeSpecial(KPEnter); + return; + case '/': + writeSpecial(PF2); + return; + case '*': + writeSpecial(PF3); + return; + /* NUMLOCK handled in keyPressed */ + default: + break; + } + /* Now what does this do and how did it get here. -Marcus + if (shift && keyChar < 32) { + write(PF1+(char)(keyChar + 64)); + return; + } + */ + } + + // FIXME: not used? + //String fmap[]; + int xind; + xind = 0; + //fmap = FunctionKey; + if (shift) { + //fmap = FunctionKeyShift; + xind = 1; + } + if (control) { + //fmap = FunctionKeyCtrl; + xind = 2; + } + if (alt) { + //fmap = FunctionKeyAlt; + xind = 3; + } + + if (keyCode == KEY_ESCAPE) { + writeSpecial(Escape[xind]); + return; + } + + if ((modifiers & VDUInput.KEY_ACTION) != 0) + switch (keyCode) { + case KEY_NUMPAD0: + writeSpecial(Numpad[0]); + return; + case KEY_NUMPAD1: + writeSpecial(Numpad[1]); + return; + case KEY_NUMPAD2: + writeSpecial(Numpad[2]); + return; + case KEY_NUMPAD3: + writeSpecial(Numpad[3]); + return; + case KEY_NUMPAD4: + writeSpecial(Numpad[4]); + return; + case KEY_NUMPAD5: + writeSpecial(Numpad[5]); + return; + case KEY_NUMPAD6: + writeSpecial(Numpad[6]); + return; + case KEY_NUMPAD7: + writeSpecial(Numpad[7]); + return; + case KEY_NUMPAD8: + writeSpecial(Numpad[8]); + return; + case KEY_NUMPAD9: + writeSpecial(Numpad[9]); + return; + case KEY_DECIMAL: + writeSpecial(NUMDot[xind]); + return; + case KEY_ADD: + writeSpecial(NUMPlus[xind]); + return; + } + + if (!((keyChar == 8) || (keyChar == 127) || (keyChar == '\r') || (keyChar == '\n'))) { + write(keyChar); + return; + } + } + + private void handle_dcs(String dcs) { + debugStr.append("DCS: ") + .append(dcs); + debug(debugStr.toString()); + debugStr.setLength(0); + } + + private void handle_osc(String osc) { + if (osc.length() > 2 && osc.substring(0, 2).equals("4;")) { + // Define color palette + String[] colorData = osc.split(";"); + + try { + int colorIndex = Integer.parseInt(colorData[1]); + + if ("rgb:".equals(colorData[2].substring(0, 4))) { + String[] rgb = colorData[2].substring(4).split("/"); + + int red = Integer.parseInt(rgb[0].substring(0, 2), 16) & 0xFF; + int green = Integer.parseInt(rgb[1].substring(0, 2), 16) & 0xFF; + int blue = Integer.parseInt(rgb[2].substring(0, 2), 16) & 0xFF; + display.setColor(colorIndex, red, green, blue); + } + } catch (Exception e) { + debugStr.append("OSC: invalid color sequence encountered: ") + .append(osc); + debug(debugStr.toString()); + debugStr.setLength(0); + } + } else + debug("OSC: " + osc); + } + + private final static char unimap[] = { + //# + //# Name: cp437_DOSLatinUS to Unicode table + //# Unicode version: 1.1 + //# Table version: 1.1 + //# Table format: Format A + //# Date: 03/31/95 + //# Authors: Michel Suignard + //# Lori Hoerth + //# General notes: none + //# + //# Format: Three tab-separated columns + //# Column #1 is the cp1255_WinHebrew code (in hex) + //# Column #2 is the Unicode (in hex as 0xXXXX) + //# Column #3 is the Unicode name (follows a comment sign, '#') + //# + //# The entries are in cp437_DOSLatinUS order + //# + + 0x0000, // #NULL + 0x0001, // #START OF HEADING + 0x0002, // #START OF TEXT + 0x0003, // #END OF TEXT + 0x0004, // #END OF TRANSMISSION + 0x0005, // #ENQUIRY + 0x0006, // #ACKNOWLEDGE + 0x0007, // #BELL + 0x0008, // #BACKSPACE + 0x0009, // #HORIZONTAL TABULATION + 0x000a, // #LINE FEED + 0x000b, // #VERTICAL TABULATION + 0x000c, // #FORM FEED + 0x000d, // #CARRIAGE RETURN + 0x000e, // #SHIFT OUT + 0x000f, // #SHIFT IN + 0x0010, // #DATA LINK ESCAPE + 0x0011, // #DEVICE CONTROL ONE + 0x0012, // #DEVICE CONTROL TWO + 0x0013, // #DEVICE CONTROL THREE + 0x0014, // #DEVICE CONTROL FOUR + 0x0015, // #NEGATIVE ACKNOWLEDGE + 0x0016, // #SYNCHRONOUS IDLE + 0x0017, // #END OF TRANSMISSION BLOCK + 0x0018, // #CANCEL + 0x0019, // #END OF MEDIUM + 0x001a, // #SUBSTITUTE + 0x001b, // #ESCAPE + 0x001c, // #FILE SEPARATOR + 0x001d, // #GROUP SEPARATOR + 0x001e, // #RECORD SEPARATOR + 0x001f, // #UNIT SEPARATOR + 0x0020, // #SPACE + 0x0021, // #EXCLAMATION MARK + 0x0022, // #QUOTATION MARK + 0x0023, // #NUMBER SIGN + 0x0024, // #DOLLAR SIGN + 0x0025, // #PERCENT SIGN + 0x0026, // #AMPERSAND + 0x0027, // #APOSTROPHE + 0x0028, // #LEFT PARENTHESIS + 0x0029, // #RIGHT PARENTHESIS + 0x002a, // #ASTERISK + 0x002b, // #PLUS SIGN + 0x002c, // #COMMA + 0x002d, // #HYPHEN-MINUS + 0x002e, // #FULL STOP + 0x002f, // #SOLIDUS + 0x0030, // #DIGIT ZERO + 0x0031, // #DIGIT ONE + 0x0032, // #DIGIT TWO + 0x0033, // #DIGIT THREE + 0x0034, // #DIGIT FOUR + 0x0035, // #DIGIT FIVE + 0x0036, // #DIGIT SIX + 0x0037, // #DIGIT SEVEN + 0x0038, // #DIGIT EIGHT + 0x0039, // #DIGIT NINE + 0x003a, // #COLON + 0x003b, // #SEMICOLON + 0x003c, // #LESS-THAN SIGN + 0x003d, // #EQUALS SIGN + 0x003e, // #GREATER-THAN SIGN + 0x003f, // #QUESTION MARK + 0x0040, // #COMMERCIAL AT + 0x0041, // #LATIN CAPITAL LETTER A + 0x0042, // #LATIN CAPITAL LETTER B + 0x0043, // #LATIN CAPITAL LETTER C + 0x0044, // #LATIN CAPITAL LETTER D + 0x0045, // #LATIN CAPITAL LETTER E + 0x0046, // #LATIN CAPITAL LETTER F + 0x0047, // #LATIN CAPITAL LETTER G + 0x0048, // #LATIN CAPITAL LETTER H + 0x0049, // #LATIN CAPITAL LETTER I + 0x004a, // #LATIN CAPITAL LETTER J + 0x004b, // #LATIN CAPITAL LETTER K + 0x004c, // #LATIN CAPITAL LETTER L + 0x004d, // #LATIN CAPITAL LETTER M + 0x004e, // #LATIN CAPITAL LETTER N + 0x004f, // #LATIN CAPITAL LETTER O + 0x0050, // #LATIN CAPITAL LETTER P + 0x0051, // #LATIN CAPITAL LETTER Q + 0x0052, // #LATIN CAPITAL LETTER R + 0x0053, // #LATIN CAPITAL LETTER S + 0x0054, // #LATIN CAPITAL LETTER T + 0x0055, // #LATIN CAPITAL LETTER U + 0x0056, // #LATIN CAPITAL LETTER V + 0x0057, // #LATIN CAPITAL LETTER W + 0x0058, // #LATIN CAPITAL LETTER X + 0x0059, // #LATIN CAPITAL LETTER Y + 0x005a, // #LATIN CAPITAL LETTER Z + 0x005b, // #LEFT SQUARE BRACKET + 0x005c, // #REVERSE SOLIDUS + 0x005d, // #RIGHT SQUARE BRACKET + 0x005e, // #CIRCUMFLEX ACCENT + 0x005f, // #LOW LINE + 0x0060, // #GRAVE ACCENT + 0x0061, // #LATIN SMALL LETTER A + 0x0062, // #LATIN SMALL LETTER B + 0x0063, // #LATIN SMALL LETTER C + 0x0064, // #LATIN SMALL LETTER D + 0x0065, // #LATIN SMALL LETTER E + 0x0066, // #LATIN SMALL LETTER F + 0x0067, // #LATIN SMALL LETTER G + 0x0068, // #LATIN SMALL LETTER H + 0x0069, // #LATIN SMALL LETTER I + 0x006a, // #LATIN SMALL LETTER J + 0x006b, // #LATIN SMALL LETTER K + 0x006c, // #LATIN SMALL LETTER L + 0x006d, // #LATIN SMALL LETTER M + 0x006e, // #LATIN SMALL LETTER N + 0x006f, // #LATIN SMALL LETTER O + 0x0070, // #LATIN SMALL LETTER P + 0x0071, // #LATIN SMALL LETTER Q + 0x0072, // #LATIN SMALL LETTER R + 0x0073, // #LATIN SMALL LETTER S + 0x0074, // #LATIN SMALL LETTER T + 0x0075, // #LATIN SMALL LETTER U + 0x0076, // #LATIN SMALL LETTER V + 0x0077, // #LATIN SMALL LETTER W + 0x0078, // #LATIN SMALL LETTER X + 0x0079, // #LATIN SMALL LETTER Y + 0x007a, // #LATIN SMALL LETTER Z + 0x007b, // #LEFT CURLY BRACKET + 0x007c, // #VERTICAL LINE + 0x007d, // #RIGHT CURLY BRACKET + 0x007e, // #TILDE + 0x007f, // #DELETE + 0x00c7, // #LATIN CAPITAL LETTER C WITH CEDILLA + 0x00fc, // #LATIN SMALL LETTER U WITH DIAERESIS + 0x00e9, // #LATIN SMALL LETTER E WITH ACUTE + 0x00e2, // #LATIN SMALL LETTER A WITH CIRCUMFLEX + 0x00e4, // #LATIN SMALL LETTER A WITH DIAERESIS + 0x00e0, // #LATIN SMALL LETTER A WITH GRAVE + 0x00e5, // #LATIN SMALL LETTER A WITH RING ABOVE + 0x00e7, // #LATIN SMALL LETTER C WITH CEDILLA + 0x00ea, // #LATIN SMALL LETTER E WITH CIRCUMFLEX + 0x00eb, // #LATIN SMALL LETTER E WITH DIAERESIS + 0x00e8, // #LATIN SMALL LETTER E WITH GRAVE + 0x00ef, // #LATIN SMALL LETTER I WITH DIAERESIS + 0x00ee, // #LATIN SMALL LETTER I WITH CIRCUMFLEX + 0x00ec, // #LATIN SMALL LETTER I WITH GRAVE + 0x00c4, // #LATIN CAPITAL LETTER A WITH DIAERESIS + 0x00c5, // #LATIN CAPITAL LETTER A WITH RING ABOVE + 0x00c9, // #LATIN CAPITAL LETTER E WITH ACUTE + 0x00e6, // #LATIN SMALL LIGATURE AE + 0x00c6, // #LATIN CAPITAL LIGATURE AE + 0x00f4, // #LATIN SMALL LETTER O WITH CIRCUMFLEX + 0x00f6, // #LATIN SMALL LETTER O WITH DIAERESIS + 0x00f2, // #LATIN SMALL LETTER O WITH GRAVE + 0x00fb, // #LATIN SMALL LETTER U WITH CIRCUMFLEX + 0x00f9, // #LATIN SMALL LETTER U WITH GRAVE + 0x00ff, // #LATIN SMALL LETTER Y WITH DIAERESIS + 0x00d6, // #LATIN CAPITAL LETTER O WITH DIAERESIS + 0x00dc, // #LATIN CAPITAL LETTER U WITH DIAERESIS + 0x00a2, // #CENT SIGN + 0x00a3, // #POUND SIGN + 0x00a5, // #YEN SIGN + 0x20a7, // #PESETA SIGN + 0x0192, // #LATIN SMALL LETTER F WITH HOOK + 0x00e1, // #LATIN SMALL LETTER A WITH ACUTE + 0x00ed, // #LATIN SMALL LETTER I WITH ACUTE + 0x00f3, // #LATIN SMALL LETTER O WITH ACUTE + 0x00fa, // #LATIN SMALL LETTER U WITH ACUTE + 0x00f1, // #LATIN SMALL LETTER N WITH TILDE + 0x00d1, // #LATIN CAPITAL LETTER N WITH TILDE + 0x00aa, // #FEMININE ORDINAL INDICATOR + 0x00ba, // #MASCULINE ORDINAL INDICATOR + 0x00bf, // #INVERTED QUESTION MARK + 0x2310, // #REVERSED NOT SIGN + 0x00ac, // #NOT SIGN + 0x00bd, // #VULGAR FRACTION ONE HALF + 0x00bc, // #VULGAR FRACTION ONE QUARTER + 0x00a1, // #INVERTED EXCLAMATION MARK + 0x00ab, // #LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + 0x00bb, // #RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + 0x2591, // #LIGHT SHADE + 0x2592, // #MEDIUM SHADE + 0x2593, // #DARK SHADE + 0x2502, // #BOX DRAWINGS LIGHT VERTICAL + 0x2524, // #BOX DRAWINGS LIGHT VERTICAL AND LEFT + 0x2561, // #BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE + 0x2562, // #BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE + 0x2556, // #BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE + 0x2555, // #BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE + 0x2563, // #BOX DRAWINGS DOUBLE VERTICAL AND LEFT + 0x2551, // #BOX DRAWINGS DOUBLE VERTICAL + 0x2557, // #BOX DRAWINGS DOUBLE DOWN AND LEFT + 0x255d, // #BOX DRAWINGS DOUBLE UP AND LEFT + 0x255c, // #BOX DRAWINGS UP DOUBLE AND LEFT SINGLE + 0x255b, // #BOX DRAWINGS UP SINGLE AND LEFT DOUBLE + 0x2510, // #BOX DRAWINGS LIGHT DOWN AND LEFT + 0x2514, // #BOX DRAWINGS LIGHT UP AND RIGHT + 0x2534, // #BOX DRAWINGS LIGHT UP AND HORIZONTAL + 0x252c, // #BOX DRAWINGS LIGHT DOWN AND HORIZONTAL + 0x251c, // #BOX DRAWINGS LIGHT VERTICAL AND RIGHT + 0x2500, // #BOX DRAWINGS LIGHT HORIZONTAL + 0x253c, // #BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL + 0x255e, // #BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE + 0x255f, // #BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE + 0x255a, // #BOX DRAWINGS DOUBLE UP AND RIGHT + 0x2554, // #BOX DRAWINGS DOUBLE DOWN AND RIGHT + 0x2569, // #BOX DRAWINGS DOUBLE UP AND HORIZONTAL + 0x2566, // #BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL + 0x2560, // #BOX DRAWINGS DOUBLE VERTICAL AND RIGHT + 0x2550, // #BOX DRAWINGS DOUBLE HORIZONTAL + 0x256c, // #BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL + 0x2567, // #BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE + 0x2568, // #BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE + 0x2564, // #BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE + 0x2565, // #BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE + 0x2559, // #BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE + 0x2558, // #BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE + 0x2552, // #BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE + 0x2553, // #BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE + 0x256b, // #BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE + 0x256a, // #BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE + 0x2518, // #BOX DRAWINGS LIGHT UP AND LEFT + 0x250c, // #BOX DRAWINGS LIGHT DOWN AND RIGHT + 0x2588, // #FULL BLOCK + 0x2584, // #LOWER HALF BLOCK + 0x258c, // #LEFT HALF BLOCK + 0x2590, // #RIGHT HALF BLOCK + 0x2580, // #UPPER HALF BLOCK + 0x03b1, // #GREEK SMALL LETTER ALPHA + 0x00df, // #LATIN SMALL LETTER SHARP S + 0x0393, // #GREEK CAPITAL LETTER GAMMA + 0x03c0, // #GREEK SMALL LETTER PI + 0x03a3, // #GREEK CAPITAL LETTER SIGMA + 0x03c3, // #GREEK SMALL LETTER SIGMA + 0x00b5, // #MICRO SIGN + 0x03c4, // #GREEK SMALL LETTER TAU + 0x03a6, // #GREEK CAPITAL LETTER PHI + 0x0398, // #GREEK CAPITAL LETTER THETA + 0x03a9, // #GREEK CAPITAL LETTER OMEGA + 0x03b4, // #GREEK SMALL LETTER DELTA + 0x221e, // #INFINITY + 0x03c6, // #GREEK SMALL LETTER PHI + 0x03b5, // #GREEK SMALL LETTER EPSILON + 0x2229, // #INTERSECTION + 0x2261, // #IDENTICAL TO + 0x00b1, // #PLUS-MINUS SIGN + 0x2265, // #GREATER-THAN OR EQUAL TO + 0x2264, // #LESS-THAN OR EQUAL TO + 0x2320, // #TOP HALF INTEGRAL + 0x2321, // #BOTTOM HALF INTEGRAL + 0x00f7, // #DIVISION SIGN + 0x2248, // #ALMOST EQUAL TO + 0x00b0, // #DEGREE SIGN + 0x2219, // #BULLET OPERATOR + 0x00b7, // #MIDDLE DOT + 0x221a, // #SQUARE ROOT + 0x207f, // #SUPERSCRIPT LATIN SMALL LETTER N + 0x00b2, // #SUPERSCRIPT TWO + 0x25a0, // #BLACK SQUARE + 0x00a0, // #NO-BREAK SPACE + }; + + public char map_cp850_unicode(char x) { + if (x >= 0x100) + return x; + return unimap[x]; + } + + private void _SetCursor(int row, int col) { + int maxr = height - 1; + int tm = getTopMargin(); + + R = (row < 0)?0: row; + C = (col < 0)?0: (col >= width) ? width - 1 : col; + + if (!moveoutsidemargins) { + R += tm; + maxr = getBottomMargin(); + } + if (R > maxr) R = maxr; + } + + private void putChar(char c, boolean isWide, boolean doshowcursor) { + int rows = this.height; //statusline + int columns = this.width; + // byte msg[]; + +// if (debug > 4) { +// debugStr.append("putChar(") +// .append(c) +// .append(" [") +// .append((int) c) +// .append("]) at R=") +// .append(R) +// .append(" , C=") +// .append(C) +// .append(", columns=") +// .append(columns) +// .append(", rows=") +// .append(rows); +// debug(debugStr.toString()); +// debugStr.setLength(0); +// } +// markLine(R, 1); +// if (c > 255) { +// if (debug > 0) +// debug("char > 255:" + (int) c); +// //return; +// } + + switch (term_state) { + case TSTATE_DATA: + /* FIXME: we shouldn't use chars with bit 8 set if ibmcharset. + * probably... but some BBS do anyway... + */ + if (!useibmcharset) { + boolean doneflag = true; + switch (c) { + case OSC: + osc = ""; + term_state = TSTATE_OSC; + break; + case RI: + if (R > getTopMargin()) + R--; + else + insertLine(R, 1, SCROLL_DOWN); + if (debug > 1) + debug("RI"); + break; + case IND: + if (debug > 2) { + debugStr.append("IND at ") + .append(R) + .append(", tm is ") + .append(getTopMargin()) + .append(", bm is ") + .append(getBottomMargin()); + debug(debugStr.toString()); + debugStr.setLength(0); + } + if (R == getBottomMargin() || R == rows - 1) + insertLine(R, 1, SCROLL_UP); + else + R++; + if (debug > 1) + debug("IND (at " + R + " )"); + break; + case NEL: + if (R == getBottomMargin() || R == rows - 1) + insertLine(R, 1, SCROLL_UP); + else + R++; + C = 0; + if (debug > 1) + debug("NEL (at " + R + " )"); + break; + case HTS: + Tabs[C] = 1; + if (debug > 1) + debug("HTS"); + break; + case DCS: + dcs = ""; + term_state = TSTATE_DCS; + break; + default: + doneflag = false; + break; + } + if (doneflag) break; + } + switch (c) { + case SS3: + onegl = 3; + break; + case SS2: + onegl = 2; + break; + case CSI: // should be in the 8bit section, but some BBS use this + DCEvar = 0; + DCEvars[0] = 0; + DCEvars[1] = 0; + DCEvars[2] = 0; + DCEvars[3] = 0; + term_state = TSTATE_CSI; + break; + case ESC: + term_state = TSTATE_ESC; + lastwaslf = 0; + break; + case 5: /* ENQ */ + write(answerBack, false); + break; + case 12: + /* FormFeed, Home for the BBS world */ + deleteArea(0, 0, columns, rows, attributes); + C = R = 0; + break; + case '\b': /* 8 */ + C--; + if (C < 0) + C = 0; + lastwaslf = 0; + break; + case '\t': + do { + // Don't overwrite or insert! TABS are not destructive, but movement! + C++; + } while (C < columns && (Tabs[C] == 0)); + lastwaslf = 0; + break; + case '\r': // 13 CR + C = 0; + break; + case '\n': // 10 LF + if (debug > 3) + debug("R= " + R + ", bm " + getBottomMargin() + ", tm=" + getTopMargin() + ", rows=" + rows); + if (!vms) { + if (lastwaslf != 0 && lastwaslf != c) // Ray: I do not understand this logic. + break; + lastwaslf = c; + /*C = 0;*/ + } + if (R == getBottomMargin() || R >= rows - 1) + insertLine(R, 1, SCROLL_UP); + else + R++; + break; + case 7: + beep(); + break; + case '\016': /* SMACS , as */ + /* ^N, Shift out - Put G1 into GL */ + gl = 1; + usedcharsets = true; + break; + case '\017': /* RMACS , ae */ + /* ^O, Shift in - Put G0 into GL */ + gl = 0; + usedcharsets = true; + break; + default: + { + int thisgl = gl; + + if (onegl >= 0) { + thisgl = onegl; + onegl = -1; + } + lastwaslf = 0; + if (c < 32) { + if (c != 0) + if (debug > 0) + debug("TSTATE_DATA char: " + ((int) c)); + /*break; some BBS really want those characters, like hearst etc. */ + if (c == 0) /* print 0 ... you bet */ + break; + } + if (C >= columns) { + if (wraparound) { + int bot = rows; + + // If we're in the scroll region, check against the bottom margin + if (R <= getBottomMargin() && R >= getTopMargin()) + bot = getBottomMargin() + 1; + + if (R < bot - 1) + R++; + else { + if (debug > 3) debug("scrolling due to wrap at " + R); + insertLine(R, 1, SCROLL_UP); + } + C = 0; + } else { + // cursor stays on last character. + C = columns - 1; + } + } + + boolean mapped = false; + + // Mapping if DEC Special is chosen charset + if (usedcharsets) { + if (c >= '\u0020' && c <= '\u007f') { + switch (gx[thisgl]) { + case '0': + // Remap SCOANSI line drawing to VT100 line drawing chars + // for our SCO using customers. + if (terminalID.equals("scoansi") || terminalID.equals("ansi")) { + for (int i = 0; i < scoansi_acs.length(); i += 2) { + if (c == scoansi_acs.charAt(i)) { + c = scoansi_acs.charAt(i + 1); + break; + } + } + } + if (c >= '\u005f' && c <= '\u007e') { + c = DECSPECIAL[(short) c - 0x5f]; + mapped = true; + } + break; + case '<': // 'user preferred' is currently 'ISO Latin-1 suppl + c = (char) ((c & 0x7f) | 0x80); + mapped = true; + break; + case 'A': + case 'B': // Latin-1 , ASCII -> fall through + mapped = true; + break; + default: + debug("Unsupported GL mapping: " + gx[thisgl]); + break; + } + } + if (!mapped && (c >= '\u0080' && c <= '\u00ff')) { + switch (gx[gr]) { + case '0': + if (c >= '\u00df' && c <= '\u00fe') { + c = DECSPECIAL[c - '\u00df']; + mapped = true; + } + break; + case '<': + case 'A': + case 'B': + mapped = true; + break; + default: + debug("Unsupported GR mapping: " + gx[gr]); + break; + } + } + } + if (!mapped && useibmcharset) + c = map_cp850_unicode(c); + + /*if(true || (statusmode == 0)) { */ + if (isWide) { + if (C >= columns - 1) { + if (wraparound) { + int bot = rows; + + // If we're in the scroll region, check against the bottom margin + if (R <= getBottomMargin() && R >= getTopMargin()) + bot = getBottomMargin() + 1; + + if (R < bot - 1) + R++; + else { + if (debug > 3) debug("scrolling due to wrap at " + R); + insertLine(R, 1, SCROLL_UP); + } + C = 0; + } else { + // cursor stays on last wide character. + C = columns - 2; + } + } + } + + if (insertmode == 1) { + if (isWide) { + insertChar(C++, R, c, attributes | FULLWIDTH); + insertChar(C, R, ' ', attributes | FULLWIDTH); + } else + insertChar(C, R, c, attributes); + } else { + if (isWide) { + putChar(C++, R, c, attributes | FULLWIDTH); + putChar(C, R, ' ', attributes | FULLWIDTH); + } else + putChar(C, R, c, attributes); + } + + /* + } else { + if (insertmode==1) { + insertChar(C, rows, c, attributes); + } else { + putChar(C, rows, c, attributes); + } + } + */ + C++; + break; + } + } /* switch(c) */ + break; + case TSTATE_OSC: + if ((c < 0x20) && (c != ESC)) {// NP - No printing character + handle_osc(osc); + term_state = TSTATE_DATA; + break; + } + //but check for vt102 ESC \ + if (c == '\\' && osc.charAt(osc.length() - 1) == ESC) { + handle_osc(osc); + term_state = TSTATE_DATA; + break; + } + osc = osc + c; + break; + case TSTATE_ESCSPACE: + term_state = TSTATE_DATA; + switch (c) { + case 'F': /* S7C1T, Disable output of 8-bit controls, use 7-bit */ + output8bit = false; + break; + case 'G': /* S8C1T, Enable output of 8-bit control codes*/ + output8bit = true; + break; + default: + debug("ESC " + c + " unhandled."); + } + break; + case TSTATE_ESC: + term_state = TSTATE_DATA; + switch (c) { + case ' ': + term_state = TSTATE_ESCSPACE; + break; + case '#': + term_state = TSTATE_ESCSQUARE; + break; + case 'c': + /* Hard terminal reset */ + reset(); + break; + case '[': + DCEvar = 0; + DCEvars[0] = 0; + DCEvars[1] = 0; + DCEvars[2] = 0; + DCEvars[3] = 0; + term_state = TSTATE_CSI; + break; + case ']': + osc = ""; + term_state = TSTATE_OSC; + break; + case 'P': + dcs = ""; + term_state = TSTATE_DCS; + break; + case 'A': /* CUU */ + R--; + if (R < 0) R = 0; + break; + case 'B': /* CUD */ + R++; + if (R >= rows) R = rows - 1; + break; + case 'C': + C++; + if (C >= columns) C = columns - 1; + break; + case 'I': // RI + insertLine(R, 1, SCROLL_DOWN); + break; + case 'E': /* NEL */ + if (R == getBottomMargin() || R == rows - 1) + insertLine(R, 1, SCROLL_UP); + else + R++; + C = 0; + if (debug > 1) + debug("ESC E (at " + R + ")"); + break; + case 'D': /* IND */ + if (R == getBottomMargin() || R == rows - 1) + insertLine(R, 1, SCROLL_UP); + else + R++; + if (debug > 1) + debug("ESC D (at " + R + " )"); + break; + case 'J': /* erase to end of screen */ + if (R < rows - 1) + deleteArea(0, R + 1, columns, rows - R - 1, attributes); + if (C < columns - 1) + deleteArea(C, R, columns - C, 1, attributes); + break; + case 'K': + if (C < columns - 1) + deleteArea(C, R, columns - C, 1, attributes); + break; + case 'M': // RI + debug("ESC M : R is "+R+", tm is "+getTopMargin()+", bm is "+getBottomMargin()); + if (R > getTopMargin()) { // just go up 1 line. + R--; + } else { // scroll down + insertLine(R, 1, SCROLL_DOWN); + } + /* else do nothing ; */ + if (debug > 2) + debug("ESC M "); + break; + case 'H': + if (debug > 1) + debug("ESC H at " + C); + /* right border probably ...*/ + if (C >= columns) + C = columns - 1; + Tabs[C] = 1; + break; + case 'N': // SS2 + onegl = 2; + break; + case 'O': // SS3 + onegl = 3; + break; + case '=': + /*application keypad*/ + if (debug > 0) + debug("ESC ="); + keypadmode = true; + break; + case '<': /* vt52 mode off */ + vt52mode = false; + break; + case '>': /*normal keypad*/ + if (debug > 0) + debug("ESC >"); + keypadmode = false; + break; + case '7': /* DECSC: save cursor, attributes */ + Sc = C; + Sr = R; + Sgl = gl; + Sgr = gr; + Sa = attributes; + Sgx = new char[4]; + for (int i = 0; i < 4; i++) Sgx[i] = gx[i]; + if (debug > 1) + debug("ESC 7"); + break; + case '8': /* DECRC: restore cursor, attributes */ + C = Sc; + R = Sr; + gl = Sgl; + gr = Sgr; + if (Sgx != null) + for (int i = 0; i < 4; i++) gx[i] = Sgx[i]; + attributes = Sa; + if (debug > 1) + debug("ESC 8"); + break; + case '(': /* Designate G0 Character set (ISO 2022) */ + term_state = TSTATE_SETG0; + usedcharsets = true; + break; + case ')': /* Designate G1 character set (ISO 2022) */ + term_state = TSTATE_SETG1; + usedcharsets = true; + break; + case '*': /* Designate G2 Character set (ISO 2022) */ + term_state = TSTATE_SETG2; + usedcharsets = true; + break; + case '+': /* Designate G3 Character set (ISO 2022) */ + term_state = TSTATE_SETG3; + usedcharsets = true; + break; + case '~': /* Locking Shift 1, right */ + gr = 1; + usedcharsets = true; + break; + case 'n': /* Locking Shift 2 */ + gl = 2; + usedcharsets = true; + break; + case '}': /* Locking Shift 2, right */ + gr = 2; + usedcharsets = true; + break; + case 'o': /* Locking Shift 3 */ + gl = 3; + usedcharsets = true; + break; + case '|': /* Locking Shift 3, right */ + gr = 3; + usedcharsets = true; + break; + case 'Y': /* vt52 cursor address mode , next chars are x,y */ + term_state = TSTATE_VT52Y; + break; + case '_': + term_state = TSTATE_TITLE; + break; + case '\\': + // TODO save title + term_state = TSTATE_DATA; + break; + default: + debug("ESC unknown letter: " + c + " (" + ((int) c) + ")"); + break; + } + break; + case TSTATE_VT52X: + C = c - 37; + if (C < 0) + C = 0; + else if (C >= width) + C = width - 1; + term_state = TSTATE_VT52Y; + break; + case TSTATE_VT52Y: + R = c - 37; + if (R < 0) + R = 0; + else if (R >= height) + R = height - 1; + term_state = TSTATE_DATA; + break; + case TSTATE_SETG0: + if (c != '0' && c != 'A' && c != 'B' && c != '<') + debug("ESC ( " + c + ": G0 char set? (" + ((int) c) + ")"); + else { + if (debug > 2) debug("ESC ( : G0 char set (" + c + " " + ((int) c) + ")"); + gx[0] = c; + } + term_state = TSTATE_DATA; + break; + case TSTATE_SETG1: + if (c != '0' && c != 'A' && c != 'B' && c != '<') { + debug("ESC ) " + c + " (" + ((int) c) + ") :G1 char set?"); + } else { + if (debug > 2) debug("ESC ) :G1 char set (" + c + " " + ((int) c) + ")"); + gx[1] = c; + } + term_state = TSTATE_DATA; + break; + case TSTATE_SETG2: + if (c != '0' && c != 'A' && c != 'B' && c != '<') + debug("ESC*:G2 char set? (" + ((int) c) + ")"); + else { + if (debug > 2) debug("ESC*:G2 char set (" + c + " " + ((int) c) + ")"); + gx[2] = c; + } + term_state = TSTATE_DATA; + break; + case TSTATE_SETG3: + if (c != '0' && c != 'A' && c != 'B' && c != '<') + debug("ESC+:G3 char set? (" + ((int) c) + ")"); + else { + if (debug > 2) debug("ESC+:G3 char set (" + c + " " + ((int) c) + ")"); + gx[3] = c; + } + term_state = TSTATE_DATA; + break; + case TSTATE_ESCSQUARE: + switch (c) { + case '8': + for (int i = 0; i < columns; i++) + for (int j = 0; j < rows; j++) + putChar(i, j, 'E', 0); + break; + default: + debug("ESC # " + c + " not supported."); + break; + } + term_state = TSTATE_DATA; + break; + case TSTATE_DCS: + if (c == '\\' && dcs.charAt(dcs.length() - 1) == ESC) { + handle_dcs(dcs); + term_state = TSTATE_DATA; + break; + } + dcs = dcs + c; + break; + + case TSTATE_DCEQ: + term_state = TSTATE_DATA; + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + DCEvars[DCEvar] = DCEvars[DCEvar] * 10 + (c) - 48; + term_state = TSTATE_DCEQ; + break; + case ';': + DCEvar++; + DCEvars[DCEvar] = 0; + term_state = TSTATE_DCEQ; + break; + case 's': // XTERM_SAVE missing! + if (true || debug > 1) + debug("ESC [ ? " + DCEvars[0] + " s unimplemented!"); + break; + case 'r': // XTERM_RESTORE + if (true || debug > 1) + debug("ESC [ ? " + DCEvars[0] + " r"); + /* DEC Mode reset */ + for (int i = 0; i <= DCEvar; i++) { + switch (DCEvars[i]) { + case 3: /* 80 columns*/ + setScreenSize(80, height, true); + break; + case 4: /* scrolling mode, smooth */ + break; + case 5: /* light background */ + break; + case 6: /* DECOM (Origin Mode) move inside margins. */ + moveoutsidemargins = true; + break; + case 7: /* DECAWM: Autowrap Mode */ + wraparound = false; + break; + case 12:/* local echo off */ + break; + case 9: /* X10 mouse */ + case 1000: /* xterm style mouse report on */ + case 1001: + case 1002: + case 1003: + mouserpt = DCEvars[i]; + break; + default: + debug("ESC [ ? " + DCEvars[0] + " r, unimplemented!"); + } + } + break; + case 'h': // DECSET + if (debug > 0) + debug("ESC [ ? " + DCEvars[0] + " h"); + /* DEC Mode set */ + for (int i = 0; i <= DCEvar; i++) { + switch (DCEvars[i]) { + case 1: /* Application cursor keys */ + KeyUp[0] = "\u001bOA"; + KeyDown[0] = "\u001bOB"; + KeyRight[0] = "\u001bOC"; + KeyLeft[0] = "\u001bOD"; + break; + case 2: /* DECANM */ + vt52mode = false; + break; + case 3: /* 132 columns*/ + setScreenSize(132, height, true); + break; + case 6: /* DECOM: move inside margins. */ + moveoutsidemargins = false; + break; + case 7: /* DECAWM: Autowrap Mode */ + wraparound = true; + break; + case 25: /* turn cursor on */ + showCursor(true); + break; + case 9: /* X10 mouse */ + case 1000: /* xterm style mouse report on */ + case 1001: + case 1002: + case 1003: + mouserpt = DCEvars[i]; + break; + + /* unimplemented stuff, fall through */ + /* 4 - scrolling mode, smooth */ + /* 5 - light background */ + /* 12 - local echo off */ + /* 18 - DECPFF - Printer Form Feed Mode -> On */ + /* 19 - DECPEX - Printer Extent Mode -> Screen */ + default: + debug("ESC [ ? " + DCEvars[0] + " h, unsupported."); + break; + } + } + break; + case 'i': // DEC Printer Control, autoprint, echo screenchars to printer + // This is different to CSI i! + // Also: "Autoprint prints a final display line only when the + // cursor is moved off the line by an autowrap or LF, FF, or + // VT (otherwise do not print the line)." + switch (DCEvars[0]) { + case 1: + if (debug > 1) + debug("CSI ? 1 i : Print line containing cursor"); + break; + case 4: + if (debug > 1) + debug("CSI ? 4 i : Start passthrough printing"); + break; + case 5: + if (debug > 1) + debug("CSI ? 4 i : Stop passthrough printing"); + break; + } + break; + case 'l': //DECRST + /* DEC Mode reset */ + if (debug > 0) + debug("ESC [ ? " + DCEvars[0] + " l"); + for (int i = 0; i <= DCEvar; i++) { + switch (DCEvars[i]) { + case 1: /* Application cursor keys */ + KeyUp[0] = "\u001b[A"; + KeyDown[0] = "\u001b[B"; + KeyRight[0] = "\u001b[C"; + KeyLeft[0] = "\u001b[D"; + break; + case 2: /* DECANM */ + vt52mode = true; + break; + case 3: /* 80 columns*/ + setScreenSize(80, height, true); + break; + case 6: /* DECOM: move outside margins. */ + moveoutsidemargins = true; + break; + case 7: /* DECAWM: Autowrap Mode OFF */ + wraparound = false; + break; + case 25: /* turn cursor off */ + showCursor(false); + break; + /* Unimplemented stuff: */ + /* 4 - scrolling mode, jump */ + /* 5 - dark background */ + /* 7 - DECAWM - no wrap around mode */ + /* 12 - local echo on */ + /* 18 - DECPFF - Printer Form Feed Mode -> Off*/ + /* 19 - DECPEX - Printer Extent Mode -> Scrolling Region */ + case 9: /* X10 mouse */ + case 1000: /* xterm style mouse report OFF */ + case 1001: + case 1002: + case 1003: + mouserpt = 0; + break; + default: + debug("ESC [ ? " + DCEvars[0] + " l, unsupported."); + break; + } + } + break; + case 'n': + if (debug > 0) + debug("ESC [ ? " + DCEvars[0] + " n"); + switch (DCEvars[0]) { + case 15: + /* printer? no printer. */ + write((ESC) + "[?13n", false); + debug("ESC[5n"); + break; + default: + debug("ESC [ ? " + DCEvars[0] + " n, unsupported."); + break; + } + break; + default: + debug("ESC [ ? " + DCEvars[0] + " " + c + ", unsupported."); + break; + } + break; + case TSTATE_CSI_EX: + term_state = TSTATE_DATA; + switch (c) { + case ESC: + term_state = TSTATE_ESC; + break; + default: + debug("Unknown character ESC[! character is " + (int) c); + break; + } + break; + case TSTATE_CSI_TICKS: + term_state = TSTATE_DATA; + switch (c) { + case 'p': + debug("Conformance level: " + DCEvars[0] + " (unsupported)," + DCEvars[1]); + if (DCEvars[0] == 61) { + output8bit = false; + break; + } + if (DCEvars[1] == 1) { + output8bit = false; + } else { + output8bit = true; /* 0 or 2 */ + } + break; + default: + debug("Unknown ESC [... \"" + c); + break; + } + break; + case TSTATE_CSI_EQUAL: + term_state = TSTATE_DATA; + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + DCEvars[DCEvar] = DCEvars[DCEvar] * 10 + (c) - 48; + term_state = TSTATE_CSI_EQUAL; + break; + case ';': + DCEvar++; + DCEvars[DCEvar] = 0; + term_state = TSTATE_CSI_EQUAL; + break; + + case 'F': /* SCO ANSI foreground */ + { + int newcolor; + + debug("ESC [ = "+DCEvars[0]+" F"); + + attributes &= ~COLOR_FG; + newcolor = ((DCEvars[0] & 1) << 2) | + (DCEvars[0] & 2) | + ((DCEvars[0] & 4) >> 2) ; + attributes |= (newcolor+1) << COLOR_FG_SHIFT; + + break; + } + case 'G': /* SCO ANSI background */ + { + int newcolor; + + debug("ESC [ = "+DCEvars[0]+" G"); + + attributes &= ~COLOR_BG; + newcolor = ((DCEvars[0] & 1) << 2) | + (DCEvars[0] & 2) | + ((DCEvars[0] & 4) >> 2) ; + attributes |= (newcolor+1) << COLOR_BG_SHIFT; + break; + } + + default: + debugStr.append("Unknown ESC [ = "); + for (int i=0;i<=DCEvar;i++) { + debugStr.append(DCEvars[i]) + .append(','); + } + debugStr.append(c); + debug(debugStr.toString()); + debugStr.setLength(0); + break; + } + break; + case TSTATE_CSI_DOLLAR: + term_state = TSTATE_DATA; + switch (c) { + case '}': + debug("Active Status Display now " + DCEvars[0]); + statusmode = DCEvars[0]; + break; + /* bad documentation? + case '-': + debug("Set Status Display now "+DCEvars[0]); + break; + */ + case '~': + debug("Status Line mode now " + DCEvars[0]); + break; + default: + debug("UNKNOWN Status Display code " + c + ", with Pn=" + DCEvars[0]); + break; + } + break; + case TSTATE_CSI: + term_state = TSTATE_DATA; + switch (c) { + case '"': + term_state = TSTATE_CSI_TICKS; + break; + case '$': + term_state = TSTATE_CSI_DOLLAR; + break; + case '=': + term_state = TSTATE_CSI_EQUAL; + break; + case '!': + term_state = TSTATE_CSI_EX; + break; + case '?': + DCEvar = 0; + DCEvars[0] = 0; + term_state = TSTATE_DCEQ; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + DCEvars[DCEvar] = DCEvars[DCEvar] * 10 + (c) - 48; + term_state = TSTATE_CSI; + break; + case ';': + DCEvar++; + DCEvars[DCEvar] = 0; + term_state = TSTATE_CSI; + break; + case 'c':/* send primary device attributes */ + /* send (ESC[?61c) */ + + String subcode = ""; + if (terminalID.equals("vt320")) subcode = "63;"; + if (terminalID.equals("vt220")) subcode = "62;"; + if (terminalID.equals("vt100")) subcode = "61;"; + write((ESC) + "[?" + subcode + "1;2c", false); + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " c"); + break; + case 'q': + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " q"); + break; + case 'g': + /* used for tabsets */ + switch (DCEvars[0]) { + case 3:/* clear them */ + Tabs = new byte[width]; + break; + case 0: + Tabs[C] = 0; + break; + } + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " g"); + break; + case 'h': + switch (DCEvars[0]) { + case 4: + insertmode = 1; + break; + case 20: + debug("Setting CRLF to TRUE"); + sendcrlf = true; + break; + default: + debug("unsupported: ESC [ " + DCEvars[0] + " h"); + break; + } + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " h"); + break; + case 'i': // Printer Controller mode. + // "Transparent printing sends all output, except the CSI 4 i + // termination string, to the printer and not the screen, + // uses an 8-bit channel if no parity so NUL and DEL will be + // seen by the printer and by the termination recognizer code, + // and all translation and character set selections are + // bypassed." + switch (DCEvars[0]) { + case 0: + if (debug > 1) + debug("CSI 0 i: Print Screen, not implemented."); + break; + case 4: + if (debug > 1) + debug("CSI 4 i: Enable Transparent Printing, not implemented."); + break; + case 5: + if (debug > 1) + debug("CSI 4/5 i: Disable Transparent Printing, not implemented."); + break; + default: + debug("ESC [ " + DCEvars[0] + " i, unimplemented!"); + } + break; + case 'l': + switch (DCEvars[0]) { + case 4: + insertmode = 0; + break; + case 20: + debug("Setting CRLF to FALSE"); + sendcrlf = false; + break; + default: + debug("ESC [ " + DCEvars[0] + " l, unimplemented!"); + break; + } + break; + case 'A': // CUU + { + int limit; + /* FIXME: xterm only cares about 0 and topmargin */ + if (R >= getTopMargin()) { + limit = getTopMargin(); + } else + limit = 0; + if (DCEvars[0] == 0) + R--; + else + R -= DCEvars[0]; + if (R < limit) + R = limit; + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " A"); + break; + } + case 'B': // CUD + /* cursor down n (1) times */ + { + int limit; + if (R <= getBottomMargin()) { + limit = getBottomMargin(); + } else + limit = rows - 1; + if (DCEvars[0] == 0) + R++; + else + R += DCEvars[0]; + if (R > limit) + R = limit; + else { + if (debug > 2) debug("Not limited."); + } + if (debug > 2) debug("to: " + R); + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " B (at C=" + C + ")"); + break; + } + case 'C': + if (DCEvars[0] == 0) + DCEvars[0] = 1; + while (DCEvars[0]-- > 0) { + C++; + } + if (C >= columns) + C = columns - 1; + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " C"); + break; + case 'd': // CVA + R = DCEvars[0] - 1; + if (R < 0) + R = 0; + else if (R >= height) + R = height - 1; + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " d"); + break; + case 'D': + if (DCEvars[0] == 0) + DCEvars[0] = 1; + while (DCEvars[0]-- > 0) { + C--; + } + if (C < 0) C = 0; + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " D"); + break; + case 'r': // DECSTBM + if (DCEvar > 0) // Ray: Any argument is optional + { + R = DCEvars[1] - 1; + if (R < 0) + R = rows - 1; + else if (R >= rows) { + R = rows - 1; + } + } else + R = rows - 1; + int bot = R; + if (R >= DCEvars[0]) { + R = DCEvars[0] - 1; + if (R < 0) + R = 0; + } + setMargins(R, bot); + _SetCursor(0, 0); + if (debug > 1) + debug("ESC [" + DCEvars[0] + " ; " + DCEvars[1] + " r"); + break; + case 'G': /* CUP / cursor absolute column */ + C = DCEvars[0]; + if (C < 0) + C = 0; + else if (C >= width) + C = width - 1; + if (debug > 1) debug("ESC [ " + DCEvars[0] + " G"); + break; + case 'H': /* CUP / cursor position */ + /* gets 2 arguments */ + _SetCursor(DCEvars[0] - 1, DCEvars[1] - 1); + if (debug > 2) { + debug("ESC [ " + DCEvars[0] + ";" + DCEvars[1] + " H, moveoutsidemargins " + moveoutsidemargins); + debug(" -> R now " + R + ", C now " + C); + } + break; + case 'f': /* move cursor 2 */ + /* gets 2 arguments */ + R = DCEvars[0] - 1; + C = DCEvars[1] - 1; + if (C < 0) + C = 0; + else if (C >= width) + C = width - 1; + if (R < 0) + R = 0; + else if (R >= height) + R = height - 1; + if (debug > 2) + debug("ESC [ " + DCEvars[0] + ";" + DCEvars[1] + " f"); + break; + case 'S': /* ind aka 'scroll forward' */ + if (DCEvars[0] == 0) + insertLine(getBottomMargin(), SCROLL_UP); + else + insertLine(getBottomMargin(), DCEvars[0], SCROLL_UP); + break; + case 'L': + /* insert n lines */ + if (DCEvars[0] == 0) + insertLine(R, SCROLL_DOWN); + else + insertLine(R, DCEvars[0], SCROLL_DOWN); + if (debug > 1) + debug("ESC [ " + DCEvars[0] + "" + (c) + " (at R " + R + ")"); + break; + case 'T': /* 'ri' aka scroll backward */ + if (DCEvars[0] == 0) + insertLine(getTopMargin(), SCROLL_DOWN); + else + insertLine(getTopMargin(), DCEvars[0], SCROLL_DOWN); + break; + case 'M': + if (debug > 1) + debug("ESC [ " + DCEvars[0] + "" + (c) + " at R=" + R); + if (DCEvars[0] == 0) + deleteLine(R); + else + for (int i = 0; i < DCEvars[0]; i++) + deleteLine(R); + break; + case 'K': + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " K"); + /* clear in line */ + switch (DCEvars[0]) { + case 6: /* 97801 uses ESC[6K for delete to end of line */ + case 0:/*clear to right*/ + if (C < columns - 1) + deleteArea(C, R, columns - C, 1, attributes); + break; + case 1:/*clear to the left, including this */ + if (C > 0) + deleteArea(0, R, C + 1, 1, attributes); + break; + case 2:/*clear whole line */ + deleteArea(0, R, columns, 1, attributes); + break; + } + break; + case 'J': + /* clear below current line */ + switch (DCEvars[0]) { + case 0: + if (R < rows - 1) + deleteArea(0, R + 1, columns, rows - R - 1, attributes); + if (C < columns - 1) + deleteArea(C, R, columns - C, 1, attributes); + break; + case 1: + if (R > 0) + deleteArea(0, 0, columns, R, attributes); + if (C > 0) + deleteArea(0, R, C + 1, 1, attributes);// include up to and including current + break; + case 2: + deleteArea(0, 0, columns, rows, attributes); + break; + } + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " J"); + break; + case '@': + if (DCEvars[0] == 0) DCEvars[0] = 1; + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " @"); + for (int i = 0; i < DCEvars[0]; i++) + insertChar(C, R, ' ', attributes); + break; + case 'X': + { + int toerase = DCEvars[0]; + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " X, C=" + C + ",R=" + R); + if (toerase == 0) + toerase = 1; + if (toerase + C > columns) + toerase = columns - C; + deleteArea(C, R, toerase, 1, attributes); + // does not change cursor position + break; + } + case 'P': + if (debug > 1) + debug("ESC [ " + DCEvars[0] + " P, C=" + C + ",R=" + R); + if (DCEvars[0] == 0) DCEvars[0] = 1; + for (int i = 0; i < DCEvars[0]; i++) + deleteChar(C, R); + break; + case 'n': + switch (DCEvars[0]) { + case 5: /* malfunction? No malfunction. */ + writeSpecial((ESC) + "[0n"); + if (debug > 1) + debug("ESC[5n"); + break; + case 6: + // DO NOT offset R and C by 1! (checked against /usr/X11R6/bin/resize + // FIXME check again. + // FIXME: but vttest thinks different??? + writeSpecial((ESC) + "[" + R + ";" + C + "R"); + if (debug > 1) + debug("ESC[6n"); + break; + default: + if (debug > 0) + debug("ESC [ " + DCEvars[0] + " n??"); + break; + } + break; + case 's': /* DECSC - save cursor */ + Sc = C; + Sr = R; + Sa = attributes; + if (debug > 3) + debug("ESC[s"); + break; + case 'u': /* DECRC - restore cursor */ + C = Sc; + R = Sr; + attributes = Sa; + if (debug > 3) + debug("ESC[u"); + break; + case 'm': /* attributes as color, bold , blink,*/ + if (debug > 3) + debug("ESC [ "); + if (DCEvar == 0 && DCEvars[0] == 0) + attributes = 0; + for (int i = 0; i <= DCEvar; i++) { + switch (DCEvars[i]) { + case 0: + if (DCEvar > 0) { + if (terminalID.equals("scoansi")) { + attributes &= COLOR; /* Keeps color. Strange but true. */ + } else { + attributes = 0; + } + } + break; + case 1: + attributes |= BOLD; + attributes &= ~LOW; + break; + case 2: + /* SCO color hack mode */ + if (terminalID.equals("scoansi") && ((DCEvar - i) >= 2)) { + int ncolor; + attributes &= ~(COLOR | BOLD); + + ncolor = DCEvars[i + 1]; + if ((ncolor & 8) == 8) + attributes |= BOLD; + ncolor = ((ncolor & 1) << 2) | (ncolor & 2) | ((ncolor & 4) >> 2); + attributes |= ((ncolor) + 1) << COLOR_FG_SHIFT; + ncolor = DCEvars[i + 2]; + ncolor = ((ncolor & 1) << 2) | (ncolor & 2) | ((ncolor & 4) >> 2); + attributes |= ((ncolor) + 1) << COLOR_BG_SHIFT; + i += 2; + } else { + attributes |= LOW; + } + break; + case 3: /* italics */ + attributes |= INVERT; + break; + case 4: + attributes |= UNDERLINE; + break; + case 7: + attributes |= INVERT; + break; + case 8: + attributes |= INVISIBLE; + break; + case 5: /* blink on */ + break; + /* 10 - ANSI X3.64-1979, select primary font, don't display control + * chars, don't set bit 8 on output */ + case 10: + gl = 0; + usedcharsets = true; + break; + /* 11 - ANSI X3.64-1979, select second alt. font, display control + * chars, set bit 8 on output */ + case 11: /* SMACS , as */ + case 12: + gl = 1; + usedcharsets = true; + break; + case 21: /* normal intensity */ + attributes &= ~(LOW | BOLD); + break; + case 23: /* italics off */ + attributes &= ~INVERT; + break; + case 25: /* blinking off */ + break; + case 27: + attributes &= ~INVERT; + break; + case 28: + attributes &= ~INVISIBLE; + break; + case 24: + attributes &= ~UNDERLINE; + break; + case 22: + attributes &= ~BOLD; + break; + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + attributes &= ~COLOR_FG; + attributes |= ((DCEvars[i] - 30) + 1)<< COLOR_FG_SHIFT; + break; + case 38: + if (DCEvars[i+1] == 5) { + attributes &= ~COLOR_FG; + attributes |= ((DCEvars[i + 2]) + 1) << COLOR_FG_SHIFT; + i += 2; + } + break; + case 39: + attributes &= ~COLOR_FG; + break; + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + attributes &= ~COLOR_BG; + attributes |= ((DCEvars[i] - 40) + 1) << COLOR_BG_SHIFT; + break; + case 48: + if (DCEvars[i+1] == 5) { + attributes &= ~COLOR_BG; + attributes |= (DCEvars[i + 2] + 1) << COLOR_BG_SHIFT; + i += 2; + } + break; + case 49: + attributes &= ~COLOR_BG; + break; + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: + attributes &= ~COLOR_FG; + attributes |= ((DCEvars[i] - 82) + 1) << COLOR_FG_SHIFT; + break; + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + attributes &= ~COLOR_BG; + attributes |= ((DCEvars[i] - 92) + 1) << COLOR_BG_SHIFT; + break; + + default: + debugStr.append("ESC [ ") + .append(DCEvars[i]) + .append(" m unknown..."); + debug(debugStr.toString()); + debugStr.setLength(0); + break; + } + if (debug > 3) { + debugStr.append(DCEvars[i]) + .append(';'); + debug(debugStr.toString()); + debugStr.setLength(0); + } + } + if (debug > 3) { + debugStr.append(" (attributes = ") + .append(attributes) + .append(")m"); + debug(debugStr.toString()); + debugStr.setLength(0); + } + break; + default: + debugStr.append("ESC [ unknown letter: ") + .append(c) + .append(" (") + .append((int)c) + .append(')'); + debug(debugStr.toString()); + debugStr.setLength(0); + break; + } + break; + case TSTATE_TITLE: + switch (c) { + case ESC: + term_state = TSTATE_ESC; + break; + default: + // TODO save title + break; + } + break; + default: + term_state = TSTATE_DATA; + break; + } + + setCursorPosition(C, R); + } + + /* hard reset the terminal */ + public void reset() { + gx[0] = 'B'; + gx[1] = 'B'; + gx[2] = 'B'; + gx[3] = 'B'; + + gl = 0; // default GL to G0 + gr = 2; // default GR to G2 + + onegl = -1; // Single shift override + + /* reset tabs */ + int nw = width; + if (nw < 132) nw = 132; + Tabs = new byte[nw]; + for (int i = 0; i < nw; i += 8) { + Tabs[i] = 1; + } + + deleteArea(0, 0, width, height, attributes); + setMargins(0, height); + C = R = 0; + _SetCursor(0, 0); + + if (display != null) + display.resetColors(); + + showCursor(true); + /*FIXME:*/ + term_state = TSTATE_DATA; + } +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Authentication.java b/app/src/main/java/net/sourceforge/jsocks/Authentication.java new file mode 100644 index 0000000..18cc48b --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Authentication.java @@ -0,0 +1,34 @@ +package net.sourceforge.jsocks; + +/** + The Authentication interface provides for performing method specific + authentication for SOCKS5 connections. +*/ +public interface Authentication{ + /** + This method is called when SOCKS5 server have selected a particular + authentication method, for whch an implementaion have been registered. + +

+ This method should return an array {inputstream,outputstream + [,UDPEncapsulation]}. The reason for that is that SOCKS5 protocol + allows to have method specific encapsulation of data on the socket for + purposes of integrity or security. And this encapsulation should be + performed by those streams returned from the method. It is also possible + to encapsulate datagrams. If authentication method supports such + encapsulation an instance of the UDPEncapsulation interface should be + returned as third element of the array, otherwise either null should be + returned as third element, or array should contain only 2 elements. + + @param methodId Authentication method selected by the server. + @param proxySocket Socket used to conect to the proxy. + @return Two or three element array containing + Input/Output streams which should be used on this connection. + Third argument is optional and should contain an instance + of UDPEncapsulation. It should be provided if the authentication + method used requires any encapsulation to be done on the + datagrams. + */ + Object[] doSocksAuthentication(int methodId,java.net.Socket proxySocket) + throws java.io.IOException; +} diff --git a/app/src/main/java/net/sourceforge/jsocks/AuthenticationNone.java b/app/src/main/java/net/sourceforge/jsocks/AuthenticationNone.java new file mode 100644 index 0000000..f28193a --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/AuthenticationNone.java @@ -0,0 +1,17 @@ +package net.sourceforge.jsocks; + +/** + SOCKS5 none authentication. Dummy class does almost nothing. +*/ +public class AuthenticationNone implements Authentication{ + + public Object[] doSocksAuthentication(int methodId, + java.net.Socket proxySocket) + throws java.io.IOException{ + + if(methodId!=0) return null; + + return new Object[] { proxySocket.getInputStream(), + proxySocket.getOutputStream()}; + } +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Proxy.java b/app/src/main/java/net/sourceforge/jsocks/Proxy.java new file mode 100644 index 0000000..381c0a0 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Proxy.java @@ -0,0 +1,404 @@ +package net.sourceforge.jsocks; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + Abstract class Proxy, base for classes Socks4Proxy and Socks5Proxy. + Defines methods for specifying default proxy, to be + used by all classes of this package. +*/ + +public abstract class Proxy{ + +//Data members + //protected InetRange directHosts = new InetRange(); + + protected InetAddress proxyIP = null; + protected String proxyHost = null; + protected int proxyPort; + protected Socket proxySocket = null; + + protected InputStream in; + protected OutputStream out; + + protected int version; + + protected Proxy chainProxy = null; + + +//Protected static/class variables + protected static Proxy defaultProxy = null; + +//Constructors +//==================== + Proxy(String proxyHost, int proxyPort) throws UnknownHostException { + this.proxyHost = proxyHost; + this.proxyIP = InetAddress.getByName(proxyHost); + this.proxyPort = proxyPort; + } + + Proxy(Proxy chainProxy,InetAddress proxyIP,int proxyPort){ + this.chainProxy = chainProxy; + this.proxyIP = proxyIP; + this.proxyPort = proxyPort; + } + + Proxy(InetAddress proxyIP,int proxyPort){ + this.proxyIP = proxyIP; + this.proxyPort = proxyPort; + } + + Proxy(Proxy p){ + this.proxyIP = p.proxyIP; + this.proxyPort = p.proxyPort; + this.version = p.version; + } + +//Public instance methods +//======================== + + /** + Get the port on which proxy server is running. + * @return Proxy port. + */ + public int getPort(){ + return proxyPort; + } + /** + Get the ip address of the proxy server host. + * @return Proxy InetAddress. + */ + public InetAddress getInetAddress(){ + return proxyIP; + } + + /** + Get string representation of this proxy. + * @returns string in the form:proxyHost:proxyPort \t Version versionNumber + */ + public String toString(){ + return (""+proxyIP.getHostName()+":"+proxyPort+"\tVersion "+version); + } + + +//Public Static(Class) Methods +//============================== + + /** + * Sets SOCKS4 proxy as default. + @param hostName Host name on which SOCKS4 server is running. + @param port Port on which SOCKS4 server is running. + @param user Username to use for communications with proxy. + */ + public static void setDefaultProxy(String hostName,int port,String user) + throws UnknownHostException{ + defaultProxy = new Socks4Proxy(hostName,port,user); + } + + /** + * Sets SOCKS4 proxy as default. + @param ipAddress Host address on which SOCKS4 server is running. + @param port Port on which SOCKS4 server is running. + @param user Username to use for communications with proxy. + */ + public static void setDefaultProxy(InetAddress ipAddress,int port, + String user){ + defaultProxy = new Socks4Proxy(ipAddress,port,user); + } + /** + * Sets SOCKS5 proxy as default. + * Default proxy only supports no-authentication. + @param hostName Host name on which SOCKS5 server is running. + @param port Port on which SOCKS5 server is running. + */ + public static void setDefaultProxy(String hostName,int port) + throws UnknownHostException{ + defaultProxy = new Socks5Proxy(hostName,port); + } + /** + * Sets SOCKS5 proxy as default. + * Default proxy only supports no-authentication. + @param ipAddress Host address on which SOCKS5 server is running. + @param port Port on which SOCKS5 server is running. + */ + public static void setDefaultProxy(InetAddress ipAddress,int port){ + defaultProxy = new Socks5Proxy(ipAddress,port); + } + /** + * Sets default proxy. + @param p Proxy to use as default proxy. + */ + public static void setDefaultProxy(Proxy p){ + defaultProxy = p; + } + + /** + Get current default proxy. + * @return Current default proxy, or null if none is set. + */ + public static Proxy getDefaultProxy(){ + return defaultProxy; + } + + /** + Parses strings in the form: host[:port:user:password], and creates + proxy from information obtained from parsing. +

+ Defaults: port = 1080.
+ If user specified but not password, creates Socks4Proxy, if user + not specified creates Socks5Proxy, if both user and password are + speciefied creates Socks5Proxy with user/password authentication. + @param proxy_entry String in the form host[:port:user:password] + @return Proxy created from the string, null if entry was somehow + invalid(host unknown for example, or empty string) + */ + public static Proxy parseProxy(String proxy_entry){ + + String proxy_host; + int proxy_port = 1080; + String proxy_user = null; + String proxy_password = null; + Proxy proxy; + + java.util.StringTokenizer st = new java.util.StringTokenizer( + proxy_entry,":"); + if(st.countTokens() < 1) return null; + + proxy_host = st.nextToken(); + if(st.hasMoreTokens()) + try{ + proxy_port = Integer.parseInt(st.nextToken().trim()); + }catch(NumberFormatException nfe){} + + if(st.hasMoreTokens()) + proxy_user = st.nextToken(); + + if(st.hasMoreTokens()) + proxy_password = st.nextToken(); + + try{ + if(proxy_user == null) + proxy = new Socks5Proxy(proxy_host,proxy_port); + else if(proxy_password == null) + proxy = new Socks4Proxy(proxy_host,proxy_port,proxy_user); + else{ + proxy = new Socks5Proxy(proxy_host,proxy_port); + /* + UserPasswordAuthentication upa = new UserPasswordAuthentication( + proxy_user, proxy_password); + + ((Socks5Proxy)proxy).setAuthenticationMethod(upa.METHOD_ID,upa); + */ + } + }catch(UnknownHostException uhe){ + return null; + } + + return proxy; + } + + +//Protected Methods +//================= + + protected void startSession()throws SocksException{ + try{ + proxySocket = new Socket(proxyIP,proxyPort); + in = proxySocket.getInputStream(); + out = proxySocket.getOutputStream(); + }catch(SocksException se){ + throw se; + }catch(IOException io_ex){ + throw new SocksException(SOCKS_PROXY_IO_ERROR,""+io_ex); + } + } + + protected abstract Proxy copy(); + protected abstract ProxyMessage formMessage(int cmd,InetAddress ip,int port); + protected abstract ProxyMessage formMessage(int cmd,String host,int port) + throws UnknownHostException; + protected abstract ProxyMessage formMessage(InputStream in) + throws SocksException, + IOException; + + + protected ProxyMessage connect(InetAddress ip,int port) + throws SocksException{ + try{ + startSession(); + ProxyMessage request = formMessage(SOCKS_CMD_CONNECT, + ip,port); + return exchange(request); + }catch(SocksException se){ + endSession(); + throw se; + } + } + protected ProxyMessage connect(String host,int port) + throws UnknownHostException,SocksException{ + try{ + startSession(); + ProxyMessage request = formMessage(SOCKS_CMD_CONNECT, + host,port); + return exchange(request); + }catch(SocksException se){ + endSession(); + throw se; + } + } + + protected ProxyMessage bind(InetAddress ip,int port) + throws SocksException{ + try{ + startSession(); + ProxyMessage request = formMessage(SOCKS_CMD_BIND, + ip,port); + return exchange(request); + }catch(SocksException se){ + endSession(); + throw se; + } + } + protected ProxyMessage bind(String host,int port) + throws UnknownHostException,SocksException{ + try{ + startSession(); + ProxyMessage request = formMessage(SOCKS_CMD_BIND, + host,port); + return exchange(request); + }catch(SocksException se){ + endSession(); + throw se; + } + } + + protected ProxyMessage accept() + throws IOException,SocksException{ + ProxyMessage msg; + try{ + msg = formMessage(in); + }catch(InterruptedIOException iioe){ + throw iioe; + }catch(IOException io_ex){ + endSession(); + throw new SocksException(SOCKS_PROXY_IO_ERROR,"While Trying accept:" + +io_ex); + } + return msg; + } + + protected ProxyMessage udpAssociate(InetAddress ip,int port) + throws SocksException{ + try{ + startSession(); + ProxyMessage request = formMessage(SOCKS_CMD_UDP_ASSOCIATE, + ip,port); + if(request != null) + return exchange(request); + }catch(SocksException se){ + endSession(); + throw se; + } + //Only get here if request was null + endSession(); + throw new SocksException(SOCKS_METHOD_NOTSUPPORTED, + "This version of proxy does not support UDP associate, use version 5"); + } + protected ProxyMessage udpAssociate(String host,int port) + throws UnknownHostException,SocksException{ + try{ + startSession(); + ProxyMessage request = formMessage(SOCKS_CMD_UDP_ASSOCIATE, + host,port); + if(request != null) return exchange(request); + }catch(SocksException se){ + endSession(); + throw se; + } + //Only get here if request was null + endSession(); + throw new SocksException(SOCKS_METHOD_NOTSUPPORTED, + "This version of proxy does not support UDP associate, use version 5"); + } + + + protected void endSession(){ + try{ + if(proxySocket!=null) proxySocket.close(); + proxySocket = null; + }catch(IOException io_ex){ + } + } + + /** + *Sends the request to SOCKS server + */ + protected void sendMsg(ProxyMessage msg)throws SocksException, + IOException{ + msg.write(out); + } + + /** + * Reads the reply from the SOCKS server + */ + protected ProxyMessage readMsg()throws SocksException, + IOException{ + return formMessage(in); + } + /** + *Sends the request reads reply and returns it + *throws exception if something wrong with IO + *or the reply code is not zero + */ + protected ProxyMessage exchange(ProxyMessage request) + throws SocksException{ + ProxyMessage reply; + try{ + request.write(out); + reply = formMessage(in); + }catch(SocksException s_ex){ + throw s_ex; + }catch(IOException ioe){ + throw(new SocksException(SOCKS_PROXY_IO_ERROR,""+ioe)); + } + return reply; + } + + +//Private methods +//=============== + + +//Constants + + public static final int SOCKS_SUCCESS =0; + public static final int SOCKS_FAILURE =1; + public static final int SOCKS_BADCONNECT =2; + public static final int SOCKS_BADNETWORK =3; + public static final int SOCKS_HOST_UNREACHABLE =4; + public static final int SOCKS_CONNECTION_REFUSED =5; + public static final int SOCKS_TTL_EXPIRE =6; + public static final int SOCKS_CMD_NOT_SUPPORTED =7; + public static final int SOCKS_ADDR_NOT_SUPPORTED =8; + + public static final int SOCKS_NO_PROXY =1<<16; + public static final int SOCKS_PROXY_NO_CONNECT =2<<16; + public static final int SOCKS_PROXY_IO_ERROR =3<<16; + public static final int SOCKS_AUTH_NOT_SUPPORTED =4<<16; + public static final int SOCKS_AUTH_FAILURE =5<<16; + public static final int SOCKS_JUST_ERROR =6<<16; + + public static final int SOCKS_DIRECT_FAILED =7<<16; + public static final int SOCKS_METHOD_NOTSUPPORTED =8<<16; + + + public static final int SOCKS_CMD_CONNECT =0x1; + static final int SOCKS_CMD_BIND =0x2; + static final int SOCKS_CMD_UDP_ASSOCIATE =0x3; + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/ProxyMessage.java b/app/src/main/java/net/sourceforge/jsocks/ProxyMessage.java new file mode 100644 index 0000000..442c380 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/ProxyMessage.java @@ -0,0 +1,109 @@ +package net.sourceforge.jsocks; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + Abstract class which describes SOCKS4/5 response/request. +*/ +public abstract class ProxyMessage{ + /** Host as an IP address */ + public InetAddress ip=null; + /** SOCKS version, or version of the response for SOCKS4*/ + public int version; + /** Port field of the request/response*/ + public int port; + /** Request/response code as an int*/ + public int command; + /** Host as string.*/ + public String host=null; + /** User field for SOCKS4 request messages*/ + public String user=null; + + ProxyMessage(int command,InetAddress ip,int port){ + this.command = command; + this.ip = ip; + this.port = port; + } + + ProxyMessage(){ + } + + + /** + Initialises Message from the stream. Reads server response from + given stream. + @param in Input stream to read response from. + @throws SocksException If server response code is not SOCKS_SUCCESS(0), or + if any error with protocol occurs. + @throws IOException If any error happens with I/O. + */ + public abstract void read(InputStream in) + throws SocksException, + IOException; + + + /** + Initialises Message from the stream. Reads server response or client + request from given stream. + + @param in Input stream to read response from. + @param clinetMode If true read server response, else read client request. + @throws SocksException If server response code is not SOCKS_SUCCESS(0) and + reading in client mode, or if any error with protocol occurs. + @throws IOException If any error happens with I/O. + */ + public abstract void read(InputStream in,boolean client_mode) + throws SocksException, + IOException; + + + /** + Writes the message to the stream. + @param out Output stream to which message should be written. + */ + public abstract void write(OutputStream out)throws SocksException, + IOException; + + /** + Get the Address field of this message as InetAddress object. + @return Host address or null, if one can't be determined. + */ + public InetAddress getInetAddress() throws UnknownHostException{ + return ip; + } + + + /** + Get string representaion of this message. + @return string representation of this message. + */ + public String toString(){ + return + "Proxy Message:\n"+ + "Version:"+ version+"\n"+ + "Command:"+ command+"\n"+ + "IP: "+ ip+"\n"+ + "Port: "+ port+"\n"+ + "User: "+ user+"\n" ; + } + +//Package methods +////////////////// + + static final String bytes2IPV4(byte[] addr,int offset){ + String hostName = ""+(addr[offset] & 0xFF); + for(int i = offset+1;i + In order to use it you will need to implement ServerAuthenticator + interface. There is an implementation of this interface which does + no authentication ServerAuthenticatorNone, but it is very dangerous + to use, as it will give access to your local network to anybody + in the world. One should never use this authentication scheme unless + one have pretty good reason to do so. + There is a couple of other authentication schemes in socks.server package. + @see socks.server.ServerAuthenticator +*/ +public class ProxyServer implements Runnable{ + + ServerAuthenticator auth; + ProxyMessage msg = null; + + Socket sock=null,remote_sock=null; + ServerSocket ss=null; + UDPRelayServer relayServer = null; + InputStream in,remote_in; + OutputStream out,remote_out; + + int mode; + static final int START_MODE = 0; + static final int ACCEPT_MODE = 1; + static final int PIPE_MODE = 2; + static final int ABORT_MODE = 3; + + static final int BUF_SIZE = 8192; + + Thread pipe_thread1,pipe_thread2; + long lastReadTime; + + protected static int iddleTimeout = 180000; //3 minutes + static int acceptTimeout = 180000; //3 minutes + + static PrintStream log = null; + static Proxy proxy; + + +//Public Constructors +///////////////////// + + + /** + Creates a proxy server with given Authentication scheme. + @param auth Authentication scheme to be used. + */ + public ProxyServer(ServerAuthenticator auth){ + this.auth = auth; + } + +//Other constructors +//////////////////// + + protected ProxyServer(ServerAuthenticator auth,Socket s){ + this.auth = auth; + this.sock = s; + mode = START_MODE; + } + +//Public methods +///////////////// + + /** + Set the logging stream. Specifying null disables logging. + */ + public static void setLog(OutputStream out){ + if(out == null){ + log = null; + }else{ + log = new PrintStream(out,true); + } + + UDPRelayServer.log = log; + } + + /** + Set proxy. +

+ Allows Proxy chaining so that one Proxy server is connected to another + and so on. If proxy supports SOCKSv4, then only some SOCKSv5 requests + can be handled, UDP would not work, however CONNECT and BIND will be + translated. + + @param p Proxy which should be used to handle user requests. + */ + public static void setProxy(Proxy p){ + proxy =p; + UDPRelayServer.proxy = proxy; + } + + /** + Get proxy. + @return Proxy wich is used to handle user requests. + */ + public static Proxy getProxy(){ + return proxy; + } + + /** + Sets the timeout for connections, how long shoud server wait + for data to arrive before dropping the connection.
+ Zero timeout implies infinity.
+ Default timeout is 3 minutes. + */ + public static void setIddleTimeout(int timeout){ + iddleTimeout = timeout; + } + /** + Sets the timeout for BIND command, how long the server should + wait for the incoming connection.
+ Zero timeout implies infinity.
+ Default timeout is 3 minutes. + */ + public static void setAcceptTimeout(int timeout){ + acceptTimeout = timeout; + } + + /** + Sets the timeout for UDPRelay server.
+ Zero timeout implies infinity.
+ Default timeout is 3 minutes. + */ + public static void setUDPTimeout(int timeout){ + UDPRelayServer.setTimeout(timeout); + } + + /** + Sets the size of the datagrams used in the UDPRelayServer.
+ Default size is 64K, a bit more than maximum possible size of the + datagram. + */ + public static void setDatagramSize(int size){ + UDPRelayServer.setDatagramSize(size); + } + + + /** + Start the Proxy server at given port.
+ This methods blocks. + */ + public void start(int port){ + start(port,5,null); + } + + /** + Create a server with the specified port, listen backlog, and local + IP address to bind to. The localIP argument can be used on a multi-homed + host for a ServerSocket that will only accept connect requests to one of + its addresses. If localIP is null, it will default accepting connections + on any/all local addresses. The port must be between 0 and 65535, + inclusive.
+ This methods blocks. + */ + public void start(int port,int backlog,InetAddress localIP){ + try{ + ss = new ServerSocket(port,backlog,localIP); + log("Starting SOCKS Proxy on:"+ss.getInetAddress().getHostAddress()+":" + +ss.getLocalPort()); + while(true){ + Socket s = ss.accept(); + log("Accepted from:"+s.getInetAddress().getHostName()+":" + +s.getPort()); + ProxyServer ps = new ProxyServer(auth,s); + (new Thread(ps)).start(); + } + }catch(IOException ioe){ + ioe.printStackTrace(); + }finally{ + } + } + + /** + Stop server operation.It would be wise to interrupt thread running the + server afterwards. + */ + public void stop(){ + try{ + if(ss != null) ss.close(); + }catch(IOException ioe){ + } + } + +//Runnable interface +//////////////////// + public void run(){ + switch(mode){ + case START_MODE: + try{ + startSession(); + }catch(IOException ioe){ + handleException(ioe); + //ioe.printStackTrace(); + }finally{ + abort(); + if(auth!=null) auth.endSession(); + log("Main thread(client->remote)stopped."); + } + break; + case ACCEPT_MODE: + try{ + doAccept(); + mode = PIPE_MODE; + pipe_thread1.interrupt(); //Tell other thread that connection have + //been accepted. + pipe(remote_in,out); + }catch(IOException ioe){ + //log("Accept exception:"+ioe); + handleException(ioe); + }finally{ + abort(); + log("Accept thread(remote->client) stopped"); + } + break; + case PIPE_MODE: + try{ + pipe(remote_in,out); + }catch(IOException ioe){ + }finally{ + abort(); + log("Support thread(remote->client) stopped"); + } + break; + case ABORT_MODE: + break; + default: + log("Unexpected MODE "+mode); + } + } + +//Private methods +///////////////// + private void startSession() throws IOException{ + sock.setSoTimeout(iddleTimeout); + + try{ + auth = auth.startSession(sock); + }catch(IOException ioe){ + log("Auth throwed exception:"+ioe); + auth = null; + return; + } + + if(auth == null){ //Authentication failed + log("Authentication failed"); + return; + } + + in = auth.getInputStream(); + out = auth.getOutputStream(); + + msg = readMsg(in); + handleRequest(msg); + } + + protected void handleRequest(ProxyMessage msg) + throws IOException{ + if(!auth.checkRequest(msg)) throw new + SocksException(Proxy.SOCKS_FAILURE); + + if(msg.ip == null){ + if(msg instanceof Socks5Message){ + msg.ip = InetAddress.getByName(msg.host); + }else + throw new SocksException(Proxy.SOCKS_FAILURE); + } + log(msg); + + switch(msg.command){ + case Proxy.SOCKS_CMD_CONNECT: + onConnect(msg); + break; + case Proxy.SOCKS_CMD_BIND: + onBind(msg); + break; + case Proxy.SOCKS_CMD_UDP_ASSOCIATE: + onUDP(msg); + break; + default: + throw new SocksException(Proxy.SOCKS_CMD_NOT_SUPPORTED); + } + } + + private void handleException(IOException ioe){ + //If we couldn't read the request, return; + if(msg == null) return; + //If have been aborted by other thread + if(mode == ABORT_MODE) return; + //If the request was successfully completed, but exception happened later + if(mode == PIPE_MODE) return; + + int error_code = Proxy.SOCKS_FAILURE; + + if(ioe instanceof SocksException) + error_code = ((SocksException)ioe).errCode; + else if(ioe instanceof NoRouteToHostException) + error_code = Proxy.SOCKS_HOST_UNREACHABLE; + else if(ioe instanceof ConnectException) + error_code = Proxy.SOCKS_CONNECTION_REFUSED; + else if(ioe instanceof InterruptedIOException) + error_code = Proxy.SOCKS_TTL_EXPIRE; + + if(error_code > Proxy.SOCKS_ADDR_NOT_SUPPORTED || error_code < 0){ + error_code = Proxy.SOCKS_FAILURE; + } + + sendErrorMessage(error_code); + } + + private void onConnect(ProxyMessage msg) throws IOException{ + Socket s; + ProxyMessage response = null; + + s = new Socket(msg.ip,msg.port); + + log("Connected to "+s.getInetAddress()+":"+s.getPort()); + + if(msg instanceof Socks5Message){ + response = new Socks5Message(Proxy.SOCKS_SUCCESS, + s.getLocalAddress(), + s.getLocalPort()); + }else{ + response = new Socks4Message(Socks4Message.REPLY_OK, + s.getLocalAddress(),s.getLocalPort()); + + } + response.write(out); + startPipe(s); + } + + private void onBind(ProxyMessage msg) throws IOException{ + ProxyMessage response = null; + + if(proxy == null) + ss = new ServerSocket(0); + else + ss = new SocksServerSocket(proxy, msg.ip, msg.port); + + ss.setSoTimeout(acceptTimeout); + + log("Trying accept on "+ss.getInetAddress()+":"+ss.getLocalPort()); + + if(msg.version == 5) + response = new Socks5Message(Proxy.SOCKS_SUCCESS,ss.getInetAddress(), + ss.getLocalPort()); + else + response = new Socks4Message(Socks4Message.REPLY_OK, + ss.getInetAddress(), + ss.getLocalPort()); + response.write(out); + + mode = ACCEPT_MODE; + + pipe_thread1 = Thread.currentThread(); + pipe_thread2 = new Thread(this); + pipe_thread2.start(); + + //Make timeout infinit. + sock.setSoTimeout(0); + int eof=0; + + try{ + while((eof=in.read())>=0){ + if(mode != ACCEPT_MODE){ + if(mode != PIPE_MODE) return;//Accept failed + + remote_out.write(eof); + break; + } + } + }catch(EOFException eofe){ + //System.out.println("EOF exception"); + return;//Connection closed while we were trying to accept. + }catch(InterruptedIOException iioe){ + //Accept thread interrupted us. + //System.out.println("Interrupted"); + if(mode != PIPE_MODE) + return;//If accept thread was not successfull return. + }finally{ + //System.out.println("Finnaly!"); + } + + if(eof < 0)//Connection closed while we were trying to accept; + return; + + //Do not restore timeout, instead timeout is set on the + //remote socket. It does not make any difference. + + pipe(in,remote_out); + } + + private void onUDP(ProxyMessage msg) throws IOException{ + if(msg.ip.getHostAddress().equals("0.0.0.0")) + msg.ip = sock.getInetAddress(); + log("Creating UDP relay server for "+msg.ip+":"+msg.port); + relayServer = new UDPRelayServer(msg.ip,msg.port, + Thread.currentThread(),sock,auth); + + ProxyMessage response; + + response = new Socks5Message(Proxy.SOCKS_SUCCESS, + relayServer.relayIP,relayServer.relayPort); + + response.write(out); + + relayServer.start(); + + //Make timeout infinit. + sock.setSoTimeout(0); + try{ + while(in.read()>=0) /*do nothing*/; + }catch(EOFException eofe){ + } + } + +//Private methods +////////////////// + + private void doAccept() throws IOException{ + Socket s; + long startTime = System.currentTimeMillis(); + + while(true){ + s = ss.accept(); + if(s.getInetAddress().equals(msg.ip)){ + //got the connection from the right host + //Close listenning socket. + ss.close(); + break; + }else if(ss instanceof SocksServerSocket){ + //We can't accept more then one connection + s.close(); + ss.close(); + throw new SocksException(Proxy.SOCKS_FAILURE); + }else{ + if(acceptTimeout!=0){ //If timeout is not infinit + int newTimeout = acceptTimeout-(int)(System.currentTimeMillis()- + startTime); + if(newTimeout <= 0) throw new InterruptedIOException( + "In doAccept()"); + ss.setSoTimeout(newTimeout); + } + s.close(); //Drop all connections from other hosts + } + } + + //Accepted connection + remote_sock = s; + remote_in = s.getInputStream(); + remote_out = s.getOutputStream(); + + //Set timeout + remote_sock.setSoTimeout(iddleTimeout); + + log("Accepted from "+s.getInetAddress()+":"+s.getPort()); + + ProxyMessage response; + + if(msg.version == 5) + response = new Socks5Message(Proxy.SOCKS_SUCCESS, s.getInetAddress(), + s.getPort()); + else + response = new Socks4Message(Socks4Message.REPLY_OK, + s.getInetAddress(), s.getPort()); + response.write(out); + } + + protected ProxyMessage readMsg(InputStream in) throws IOException{ + PushbackInputStream push_in; + if(in instanceof PushbackInputStream) + push_in = (PushbackInputStream) in; + else + push_in = new PushbackInputStream(in); + + int version = push_in.read(); + push_in.unread(version); + + + ProxyMessage msg; + + if(version == 5){ + msg = new Socks5Message(push_in,false); + }else if(version == 4){ + msg = new Socks4Message(push_in,false); + }else{ + throw new SocksException(Proxy.SOCKS_FAILURE); + } + return msg; + } + + private void startPipe(Socket s){ + mode = PIPE_MODE; + remote_sock = s; + try{ + remote_in = s.getInputStream(); + remote_out = s.getOutputStream(); + pipe_thread1 = Thread.currentThread(); + pipe_thread2 = new Thread(this); + pipe_thread2.start(); + pipe(in,remote_out); + }catch(IOException ioe){ + } + } + + private void sendErrorMessage(int error_code){ + ProxyMessage err_msg; + if(msg instanceof Socks4Message) + err_msg = new Socks4Message(Socks4Message.REPLY_REJECTED); + else + err_msg = new Socks5Message(error_code); + try{ + err_msg.write(out); + }catch(IOException ioe){} + } + + private synchronized void abort(){ + if(mode == ABORT_MODE) return; + mode = ABORT_MODE; + try{ + log("Aborting operation"); + if(remote_sock != null) remote_sock.close(); + if(sock != null) sock.close(); + if(relayServer!=null) relayServer.stop(); + if(ss!=null) ss.close(); + if(pipe_thread1 != null) pipe_thread1.interrupt(); + if(pipe_thread2 != null) pipe_thread2.interrupt(); + }catch(IOException ioe){} + } + + static final void log(String s){ + if(log != null){ + log.println(s); + log.flush(); + } + } + + static final void log(ProxyMessage msg){ + log("Request version:"+msg.version+ + "\tCommand: "+command2String(msg.command)); + log("IP:"+msg.ip +"\tPort:"+msg.port+ + (msg.version==4?"\tUser:"+msg.user:"")); + } + + private void pipe(InputStream in,OutputStream out) throws IOException{ + lastReadTime = System.currentTimeMillis(); + byte[] buf = new byte[BUF_SIZE]; + int len = 0; + while(len >= 0){ + try{ + if(len!=0){ + out.write(buf,0,len); + out.flush(); + } + len= in.read(buf); + lastReadTime = System.currentTimeMillis(); + }catch(InterruptedIOException iioe){ + if(iddleTimeout == 0) return;//Other thread interrupted us. + long timeSinceRead = System.currentTimeMillis() - lastReadTime; + if(timeSinceRead >= iddleTimeout - 1000) //-1s for adjustment. + return; + len = 0; + + } + } + } + static final String command_names[] = {"CONNECT","BIND","UDP_ASSOCIATE"}; + + static final String command2String(int cmd){ + if(cmd > 0 && cmd < 4) return command_names[cmd-1]; + else return "Unknown Command "+cmd; + } +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Socks4Message.java b/app/src/main/java/net/sourceforge/jsocks/Socks4Message.java new file mode 100644 index 0000000..99fb211 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Socks4Message.java @@ -0,0 +1,171 @@ +package net.sourceforge.jsocks; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + SOCKS4 Reply/Request message. +*/ + +public class Socks4Message extends ProxyMessage{ + + private byte[] msgBytes; + private int msgLength; + + /** + * Server failed reply, cmd command for failed request + */ + public Socks4Message(int cmd){ + super(cmd,null,0); + this.user = null; + + msgLength = 2; + msgBytes = new byte[2]; + + msgBytes[0] = (byte) 0; + msgBytes[1] = (byte) command; + } + + /** + * Server successfull reply + */ + public Socks4Message(int cmd,InetAddress ip,int port){ + this(0,cmd,ip,port,null); + } + + /** + * Client request + */ + public Socks4Message(int cmd,InetAddress ip,int port,String user){ + this(SOCKS_VERSION,cmd,ip,port,user); + } + + /** + * Most general constructor + */ + public Socks4Message(int version, int cmd, + InetAddress ip,int port,String user){ + super(cmd,ip,port); + this.user = user; + this.version = version; + + msgLength = user == null?8:9+user.length(); + msgBytes = new byte[msgLength]; + + msgBytes[0] = (byte) version; + msgBytes[1] = (byte) command; + msgBytes[2] = (byte) (port >> 8); + msgBytes[3] = (byte) port; + + byte[] addr; + + if(ip != null) + addr = ip.getAddress(); + else{ + addr = new byte[4]; + addr[0]=addr[1]=addr[2]=addr[3]=0; + } + System.arraycopy(addr,0,msgBytes,4,4); + + if(user != null){ + byte[] buf = user.getBytes(); + System.arraycopy(buf,0,msgBytes,8,buf.length); + msgBytes[msgBytes.length -1 ] = 0; + } + } + + /** + *Initialise from the stream + *If clientMode is true attempts to read a server response + *otherwise reads a client request + *see read for more detail + */ + public Socks4Message(InputStream in, boolean clientMode) throws IOException{ + msgBytes = null; + read(in,clientMode); + } + + @Override +public void read(InputStream in) throws IOException{ + read(in,true); + } + + @Override +public void read(InputStream in, boolean clientMode) throws IOException{ + boolean mode4a = false; + DataInputStream d_in = new DataInputStream(in); + version= d_in.readUnsignedByte(); + command = d_in.readUnsignedByte(); + if(clientMode && command != REPLY_OK){ + String errMsg; + if(command >REPLY_OK && command < REPLY_BAD_IDENTD) + errMsg = replyMessage[command-REPLY_OK]; + else + errMsg = "Unknown Reply Code"; + throw new SocksException(command,errMsg); + } + port = d_in.readUnsignedShort(); + byte[] addr = new byte[4]; + d_in.readFully(addr); + if (addr[0] == 0 && addr[1] == 0 && addr[2] == 0 && addr[3] != 0) + mode4a = true; + else { + ip=bytes2IP(addr); + host = ip.getHostName(); + } + if(!clientMode){ + StringBuilder sb = new StringBuilder(); + int b; + while ((b = in.read()) != 0) + sb.append((char) b); + user = sb.toString(); + if (mode4a) { + sb.setLength(0); + while ((b = in.read()) != 0) + sb.append((char) b); + host = sb.toString(); + } + } + } + @Override +public void write(OutputStream out) throws IOException{ + if(msgBytes == null){ + Socks4Message msg = new Socks4Message(version,command,ip,port,user); + msgBytes = msg.msgBytes; + msgLength = msg.msgLength; + } + out.write(msgBytes); + } + + //Class methods + static InetAddress bytes2IP(byte[] addr){ + String s = bytes2IPV4(addr,0); + try{ + return InetAddress.getByName(s); + }catch(UnknownHostException uh_ex){ + return null; + } + } + + //Constants + + static final String[] replyMessage ={ + "Request Granted", + "Request Rejected or Failed", + "Failed request, can't connect to Identd", + "Failed request, bad user name"}; + + static final int SOCKS_VERSION = 4; + + public final static int REQUEST_CONNECT = 1; + public final static int REQUEST_BIND = 2; + + public final static int REPLY_OK = 90; + public final static int REPLY_REJECTED = 91; + public final static int REPLY_NO_CONNECT = 92; + public final static int REPLY_BAD_IDENTD = 93; + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Socks4Proxy.java b/app/src/main/java/net/sourceforge/jsocks/Socks4Proxy.java new file mode 100644 index 0000000..9a17fc2 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Socks4Proxy.java @@ -0,0 +1,107 @@ +package net.sourceforge.jsocks; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + Proxy which describes SOCKS4 proxy. +*/ + +public class Socks4Proxy extends Proxy implements Cloneable{ + +//Data members + String user; + +//Public Constructors +//==================== + + /** + Creates the SOCKS4 proxy + @param p Proxy to use to connect to this proxy, allows proxy chaining. + @param proxyHost Address of the proxy server. + @param proxyPort Port of the proxy server + @param user User name to use for identification purposes. + @throws UnknownHostException If proxyHost can't be resolved. + */ + public Socks4Proxy(String proxyHost,int proxyPort,String user) + throws UnknownHostException{ + super(proxyHost,proxyPort); + this.user = new String(user); + version = 4; + } + + /** + Creates the SOCKS4 proxy + @param p Proxy to use to connect to this proxy, allows proxy chaining. + @param proxyIP Address of the proxy server. + @param proxyPort Port of the proxy server + @param user User name to use for identification purposes. + */ + public Socks4Proxy(Proxy p,InetAddress proxyIP,int proxyPort,String user){ + super(p,proxyIP,proxyPort); + this.user = new String(user); + version = 4; + } + + /** + Creates the SOCKS4 proxy + @param proxyIP Address of the proxy server. + @param proxyPort Port of the proxy server + @param user User name to use for identification purposes. + */ + public Socks4Proxy(InetAddress proxyIP,int proxyPort,String user){ + this(null,proxyIP,proxyPort,user); + } + +//Public instance methods +//======================== + + /** + * Creates a clone of this proxy. Changes made to the clone should not + * affect this object. + */ + public Object clone(){ + Socks4Proxy newProxy = new Socks4Proxy(proxyIP,proxyPort,user); + newProxy.chainProxy = chainProxy; + return newProxy; + } + + +//Public Static(Class) Methods +//============================== + + +//Protected Methods +//================= + + protected Proxy copy(){ + Socks4Proxy copy = new Socks4Proxy(proxyIP,proxyPort,user); + copy.chainProxy = chainProxy; + return copy; + } + + protected ProxyMessage formMessage(int cmd,InetAddress ip,int port){ + switch(cmd){ + case SOCKS_CMD_CONNECT: + cmd = Socks4Message.REQUEST_CONNECT; + break; + case SOCKS_CMD_BIND: + cmd = Socks4Message.REQUEST_BIND; + break; + default: + return null; + } + return new Socks4Message(cmd,ip,port,user); + } + protected ProxyMessage formMessage(int cmd,String host,int port) + throws UnknownHostException{ + return formMessage(cmd,InetAddress.getByName(host),port); + } + protected ProxyMessage formMessage(InputStream in) + throws SocksException, + IOException{ + return new Socks4Message(in,true); + } + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Socks5DatagramSocket.java b/app/src/main/java/net/sourceforge/jsocks/Socks5DatagramSocket.java new file mode 100644 index 0000000..b847400 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Socks5DatagramSocket.java @@ -0,0 +1,460 @@ +package net.sourceforge.jsocks; +import java.net.*; +import java.io.*; + +/** + Datagram socket to interract through the firewall.
+ Can be used same way as the normal DatagramSocket. One should + be carefull though with the datagram sizes used, as additional data + is present in both incomming and outgoing datagrams. +

+ SOCKS5 protocol allows to send host address as either: +

    +
  • IPV4, normal 4 byte address. (10 bytes header size) +
  • IPV6, version 6 ip address (not supported by Java as for now). + 22 bytes header size. +
  • Host name,(7+length of the host name bytes header size). +
+ As with other Socks equivalents, direct addresses are handled + transparently, that is data will be send directly when required + by the proxy settings. +

+ NOTE:
+ Unlike other SOCKS Sockets, it does not support proxy chaining, + and will throw an exception if proxy has a chain proxy attached. The + reason for that is not my laziness, but rather the restrictions of + the SOCKSv5 protocol. Basicaly SOCKSv5 proxy server, needs to know from + which host:port datagrams will be send for association, and returns address + to which datagrams should be send by the client, but it does not + inform client from which host:port it is going to send datagrams, in fact + there is even no guarantee they will be send at all and from the same address + each time. + + */ +public class Socks5DatagramSocket extends DatagramSocket{ + + InetAddress relayIP; + int relayPort; + Socks5Proxy proxy; + private boolean server_mode = false; + UDPEncapsulation encapsulation; + + + /** + Construct Datagram socket for communication over SOCKS5 proxy + server. This constructor uses default proxy, the one set with + Proxy.setDefaultProxy() method. If default proxy is not set or + it is set to version4 proxy, which does not support datagram + forwarding, throws SocksException. + + */ + public Socks5DatagramSocket() throws SocksException, + IOException{ + this(Proxy.defaultProxy,0,null); + } + /** + Construct Datagram socket for communication over SOCKS5 proxy + server. And binds it to the specified local port. + This constructor uses default proxy, the one set with + Proxy.setDefaultProxy() method. If default proxy is not set or + it is set to version4 proxy, which does not support datagram + forwarding, throws SocksException. + */ + public Socks5DatagramSocket(int port) throws SocksException, + IOException{ + this(Proxy.defaultProxy,port,null); + } + /** + Construct Datagram socket for communication over SOCKS5 proxy + server. And binds it to the specified local port and address. + This constructor uses default proxy, the one set with + Proxy.setDefaultProxy() method. If default proxy is not set or + it is set to version4 proxy, which does not support datagram + forwarding, throws SocksException. + */ + public Socks5DatagramSocket(int port,InetAddress ip) throws SocksException, + IOException{ + this(Proxy.defaultProxy,port,ip); + } + + /** + Constructs datagram socket for communication over specified proxy. + And binds it to the given local address and port. Address of null + and port of 0, signify any availabale port/address. + Might throw SocksException, if: +

    +
  1. Given version of proxy does not support UDP_ASSOCIATE. +
  2. Proxy can't be reached. +
  3. Authorization fails. +
  4. Proxy does not want to perform udp forwarding, for any reason. +
+ Might throw IOException if binding dtagram socket to given address/port + fails. + See java.net.DatagramSocket for more details. + */ + public Socks5DatagramSocket(Proxy p,int port,InetAddress ip) + throws SocksException, + IOException{ + super(port,ip); + if(p == null) throw new SocksException(Proxy.SOCKS_NO_PROXY); + if(!(p instanceof Socks5Proxy)) + throw new SocksException(-1,"Datagram Socket needs Proxy version 5"); + + if(p.chainProxy != null) + throw new SocksException(Proxy.SOCKS_JUST_ERROR, + "Datagram Sockets do not support proxy chaining."); + + proxy =(Socks5Proxy) p.copy(); + + ProxyMessage msg = proxy.udpAssociate(super.getLocalAddress(), + super.getLocalPort()); + relayIP = msg.ip; + if(relayIP.getHostAddress().equals("0.0.0.0")) relayIP = proxy.proxyIP; + relayPort = msg.port; + + encapsulation = proxy.udp_encapsulation; + + //debug("Datagram Socket:"+getLocalAddress()+":"+getLocalPort()+"\n"); + //debug("Socks5Datagram: "+relayIP+":"+relayPort+"\n"); + } + + /** + Used by UDPRelayServer. + */ + Socks5DatagramSocket(boolean server_mode,UDPEncapsulation encapsulation, + InetAddress relayIP,int relayPort) + throws IOException{ + super(); + this.server_mode = server_mode; + this.relayIP = relayIP; + this.relayPort = relayPort; + this.encapsulation = encapsulation; + this.proxy = null; + } + + /** + Sends the Datagram either through the proxy or directly depending + on current proxy settings and destination address.
+ + NOTE: DatagramPacket size should be at least 10 bytes less + than the systems limit. + +

+ See documentation on java.net.DatagramSocket + for full details on how to use this method. + @param dp Datagram to send. + @throws IOException If error happens with I/O. + */ + public void send(DatagramPacket dp) throws IOException{ + //If the host should be accessed directly, send it as is. + if(!server_mode){ + super.send(dp); + //debug("Sending directly:"); + return; + } + + byte[] head = formHeader(dp.getAddress(),dp.getPort()); + byte[] buf = new byte[head.length + dp.getLength()]; + byte[] data = dp.getData(); + //Merge head and data + System.arraycopy(head,0,buf,0,head.length); + //System.arraycopy(data,dp.getOffset(),buf,head.length,dp.getLength()); + System.arraycopy(data,0,buf,head.length,dp.getLength()); + + if(encapsulation != null) + buf = encapsulation.udpEncapsulate(buf,true); + + super.send(new DatagramPacket(buf,buf.length,relayIP,relayPort)); + } + /** + This method allows to send datagram packets with address type DOMAINNAME. + SOCKS5 allows to specify host as names rather than ip addresses.Using + this method one can send udp datagrams through the proxy, without having + to know the ip address of the destination host. +

+ If proxy specified for that socket has an option resolveAddrLocally set + to true host will be resolved, and the datagram will be send with address + type IPV4, if resolve fails, UnknownHostException is thrown. + @param dp Datagram to send, it should contain valid port and data + @param host Host name to which datagram should be send. + @throws IOException If error happens with I/O, or the host can't be + resolved when proxy settings say that hosts should be resolved locally. + @see Socks5Proxy#resolveAddrLocally(boolean) + */ + public void send(DatagramPacket dp, String host) throws IOException { + dp.setAddress(InetAddress.getByName(host)); + super.send(dp); + } + + /** + * Receives udp packet. If packet have arrived from the proxy relay server, + * it is processed and address and port of the packet are set to the + * address and port of sending host.
+ * If the packet arrived from anywhere else it is not changed.
+ * NOTE: DatagramPacket size should be at least 10 bytes bigger + * than the largest packet you expect (this is for IPV4 addresses). + * For hostnames and IPV6 it is even more. + @param dp Datagram in which all relevent information will be copied. + */ + public void receive(DatagramPacket dp) throws IOException{ + super.receive(dp); + + if(server_mode){ + //Drop all datagrams not from relayIP/relayPort + int init_length = dp.getLength(); + int initTimeout = getSoTimeout(); + long startTime = System.currentTimeMillis(); + + while(!relayIP.equals(dp.getAddress()) || + relayPort != dp.getPort()){ + + //Restore datagram size + dp.setLength(init_length); + + //If there is a non-infinit timeout on this socket + //Make sure that it happens no matter how often unexpected + //packets arrive. + if(initTimeout != 0){ + int newTimeout = initTimeout - (int)(System.currentTimeMillis() - + startTime); + if(newTimeout <= 0) throw new InterruptedIOException( + "In Socks5DatagramSocket->receive()"); + setSoTimeout(newTimeout); + } + + super.receive(dp); + } + + //Restore timeout settings + if(initTimeout != 0) setSoTimeout(initTimeout); + + }else if(!relayIP.equals(dp.getAddress()) || + relayPort != dp.getPort()) + return; // Recieved direct packet + //If the datagram is not from the relay server, return it it as is. + + byte[] data; + data = dp.getData(); + + if(encapsulation != null) + data = encapsulation.udpEncapsulate(data,false); + + int offset = 0; //Java 1.1 + //int offset = dp.getOffset(); //Java 1.2 + + ByteArrayInputStream bIn = new ByteArrayInputStream(data,offset, + dp.getLength()); + + + ProxyMessage msg = new Socks5Message(bIn); + dp.setPort(msg.port); + dp.setAddress(msg.getInetAddress()); + + //what wasn't read by the Message is the data + int data_length = bIn.available(); + //Shift data to the left + System.arraycopy(data,offset+dp.getLength()-data_length, + data,offset,data_length); + + + dp.setLength(data_length); + } + + /** + * Returns port assigned by the proxy, to which datagrams are relayed. + * It is not the same port to which other party should send datagrams. + @return Port assigned by socks server to which datagrams are send + for association. + */ + public int getLocalPort(){ + if(server_mode) return super.getLocalPort(); + return relayPort; + } + /** + * Address assigned by the proxy, to which datagrams are send for relay. + * It is not necesseraly the same address, to which other party should send + * datagrams. + @return Address to which datagrams are send for association. + */ + public InetAddress getLocalAddress(){ + if(server_mode) return super.getLocalAddress(); + return relayIP; + } + + /** + * Closes datagram socket, and proxy connection. + */ + public void close(){ + if(!server_mode) proxy.endSession(); + super.close(); + } + + /** + This method checks wether proxy still runs udp forwarding service + for this socket. +

+ This methods checks wether the primary connection to proxy server + is active. If it is, chances are that proxy continues to forward + datagrams being send from this socket. If it was closed, most likely + datagrams are no longer being forwarded by the server. +

+ Proxy might decide to stop forwarding datagrams, in which case it + should close primary connection. This method allows to check, wether + this have been done. +

+ You can specify timeout for which we should be checking EOF condition + on the primary connection. Timeout is in milliseconds. Specifying 0 as + timeout implies infinity, in which case method will block, until + connection to proxy is closed or an error happens, and then return false. +

+ One possible scenario is to call isProxyactive(0) in separate thread, + and once it returned notify other threads about this event. + + @param timeout For how long this method should block, before returning. + @return true if connection to proxy is active, false if eof or error + condition have been encountered on the connection. + */ + public boolean isProxyAlive(int timeout){ + if(server_mode) return false; + if(proxy != null){ + try{ + proxy.proxySocket.setSoTimeout(timeout); + + int eof = proxy.in.read(); + if(eof < 0) return false; // EOF encountered. + else return true; // This really should not happen + + }catch(InterruptedIOException iioe){ + return true; // read timed out. + }catch(IOException ioe){ + return false; + } + } + return false; + } + +//PRIVATE METHODS +////////////////// + + + private byte[] formHeader(InetAddress ip, int port){ + Socks5Message request = new Socks5Message(0,ip,port); + request.data[0] = 0; + return request.data; + } + + +/*====================================================================== + +//Mainly Test functions +////////////////////// + + private String bytes2String(byte[] b){ + String s=""; + char[] hex_digit = { '0','1','2','3','4','5','6','7','8','9', + 'A','B','C','D','E','F'}; + for(int i=0;i> 4; + int i2 = b[i] & 0xF; + s+=hex_digit[i1]; + s+=hex_digit[i2]; + s+=" "; + } + return s; + } + private static final void debug(String s){ + if(DEBUG) + System.out.print(s); + } + + private static final boolean DEBUG = true; + + + public static void usage(){ + System.err.print( + "Usage: java Socks.SocksDatagramSocket host port [socksHost socksPort]\n"); + } + + static final int defaultProxyPort = 1080; //Default Port + static final String defaultProxyHost = "www-proxy"; //Default proxy + + public static void main(String args[]){ + int port; + String host; + int proxyPort; + String proxyHost; + InetAddress ip; + + if(args.length > 1 && args.length < 5){ + try{ + + host = args[0]; + port = Integer.parseInt(args[1]); + + proxyPort =(args.length > 3)? Integer.parseInt(args[3]) + : defaultProxyPort; + + host = args[0]; + ip = InetAddress.getByName(host); + + proxyHost =(args.length > 2)? args[2] + : defaultProxyHost; + + Proxy.setDefaultProxy(proxyHost,proxyPort); + Proxy p = Proxy.getDefaultProxy(); + p.addDirect("lux"); + + + DatagramSocket ds = new Socks5DatagramSocket(); + + + BufferedReader in = new BufferedReader( + new InputStreamReader(System.in)); + String s; + + System.out.print("Enter line:"); + s = in.readLine(); + + while(s != null){ + byte[] data = (s+"\r\n").getBytes(); + DatagramPacket dp = new DatagramPacket(data,0,data.length, + ip,port); + System.out.println("Sending to: "+ip+":"+port); + ds.send(dp); + dp = new DatagramPacket(new byte[1024],1024); + + System.out.println("Trying to recieve on port:"+ + ds.getLocalPort()); + ds.receive(dp); + System.out.print("Recieved:\n"+ + "From:"+dp.getAddress()+":"+dp.getPort()+ + "\n\n"+ + new String(dp.getData(),dp.getOffset(),dp.getLength())+"\n" + ); + System.out.print("Enter line:"); + s = in.readLine(); + + } + ds.close(); + System.exit(1); + + }catch(SocksException s_ex){ + System.err.println("SocksException:"+s_ex); + s_ex.printStackTrace(); + System.exit(1); + }catch(IOException io_ex){ + io_ex.printStackTrace(); + System.exit(1); + }catch(NumberFormatException num_ex){ + usage(); + num_ex.printStackTrace(); + System.exit(1); + } + + }else{ + usage(); + } + } +*/ + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Socks5Message.java b/app/src/main/java/net/sourceforge/jsocks/Socks5Message.java new file mode 100644 index 0000000..ea2a321 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Socks5Message.java @@ -0,0 +1,292 @@ +package net.sourceforge.jsocks; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + SOCKS5 request/response message. +*/ + +public class Socks5Message extends ProxyMessage{ + /** Address type of given message*/ + public int addrType; + + byte[] data; + + /** + Server error response. + @param cmd Error code. + */ + public Socks5Message(int cmd){ + super(cmd,null,0); + data = new byte[3]; + data[0] = SOCKS_VERSION; //Version. + data[1] = (byte)cmd; //Reply code for some kind of failure. + data[2] = 0; //Reserved byte. + } + + /** + Construct client request or server response. + @param cmd - Request/Response code. + @param ip - IP field. + @paarm port - port field. + */ + public Socks5Message(int cmd,InetAddress ip,int port){ + super(cmd,ip,port); + this.host = ip==null?"0.0.0.0":ip.getHostName(); + this.version = SOCKS_VERSION; + + byte[] addr; + + if(ip == null){ + addr = new byte[4]; + addr[0]=addr[1]=addr[2]=addr[3]=0; + }else + addr = ip.getAddress(); + + addrType = addr.length == 4 ? SOCKS_ATYP_IPV4 + : SOCKS_ATYP_IPV6; + + data = new byte[6+addr.length]; + data[0] = (byte) SOCKS_VERSION; //Version + data[1] = (byte) command; //Command + data[2] = (byte) 0; //Reserved byte + data[3] = (byte) addrType; //Address type + + //Put Address + System.arraycopy(addr,0,data,4,addr.length); + //Put port + data[data.length-2] = (byte)(port>>8); + data[data.length-1] = (byte)(port); + } + + + /** + Construct client request or server response. + @param cmd - Request/Response code. + @param hostName - IP field as hostName, uses ADDR_TYPE of HOSTNAME. + @paarm port - port field. + */ + public Socks5Message(int cmd,String hostName,int port){ + super(cmd,null,port); + this.host = hostName; + this.version = SOCKS_VERSION; + + //System.out.println("Doing ATYP_DOMAINNAME"); + + addrType = SOCKS_ATYP_DOMAINNAME; + byte addr[] = hostName.getBytes(); + + data =new byte[7+addr.length]; + data[0] = (byte) SOCKS_VERSION; //Version + data[1] = (byte) command; //Command + data[2] = (byte) 0; //Reserved byte + data[3] = (byte) SOCKS_ATYP_DOMAINNAME; //Address type + data[4] = (byte) addr.length; //Length of the address + + //Put Address + System.arraycopy(addr,0,data,5,addr.length); + //Put port + data[data.length-2] = (byte)(port >>8); + data[data.length-1] = (byte)(port); + } + + /** + Initialises Message from the stream. Reads server response from + given stream. + @param in Input stream to read response from. + @throws SocksException If server response code is not SOCKS_SUCCESS(0), or + if any error with protocol occurs. + @throws IOException If any error happens with I/O. + */ + public Socks5Message(InputStream in) throws SocksException, + IOException{ + this(in,true); + } + + /** + Initialises Message from the stream. Reads server response or client + request from given stream. + + @param in Input stream to read response from. + @param clinetMode If true read server response, else read client request. + @throws SocksException If server response code is not SOCKS_SUCCESS(0) and + reading in client mode, or if any error with protocol occurs. + @throws IOException If any error happens with I/O. + */ + public Socks5Message(InputStream in,boolean clientMode)throws SocksException, + IOException{ + read(in,clientMode); + } + + + /** + Initialises Message from the stream. Reads server response from + given stream. + @param in Input stream to read response from. + @throws SocksException If server response code is not SOCKS_SUCCESS(0), or + if any error with protocol occurs. + @throws IOException If any error happens with I/O. + */ + public void read(InputStream in) throws SocksException, + IOException{ + read(in,true); + } + + + /** + Initialises Message from the stream. Reads server response or client + request from given stream. + + @param in Input stream to read response from. + @param clinetMode If true read server response, else read client request. + @throws SocksException If server response code is not SOCKS_SUCCESS(0) and + reading in client mode, or if any error with protocol occurs. + @throws IOException If any error happens with I/O. + */ + public void read(InputStream in,boolean clientMode) throws SocksException, + IOException{ + data = null; + ip = null; + + DataInputStream di = new DataInputStream(in); + + version = di.readUnsignedByte(); + command = di.readUnsignedByte(); + if(clientMode && command != 0) + throw new SocksException(command); + + @SuppressWarnings("unused") + int reserved = di.readUnsignedByte(); + addrType = di.readUnsignedByte(); + + byte addr[]; + + switch(addrType){ + case SOCKS_ATYP_IPV4: + addr = new byte[4]; + di.readFully(addr); + host = bytes2IPV4(addr,0); + break; + case SOCKS_ATYP_IPV6: + addr = new byte[SOCKS_IPV6_LENGTH];//I believe it is 16 bytes,huge! + di.readFully(addr); + host = bytes2IPV6(addr,0); + break; + case SOCKS_ATYP_DOMAINNAME: + //System.out.println("Reading ATYP_DOMAINNAME"); + addr = new byte[di.readUnsignedByte()];//Next byte shows the length + di.readFully(addr); + host = new String(addr); + break; + default: + throw(new SocksException(Proxy.SOCKS_JUST_ERROR)); + } + + port = di.readUnsignedShort(); + + if(addrType != SOCKS_ATYP_DOMAINNAME && doResolveIP){ + try{ + ip = InetAddress.getByName(host); + }catch(UnknownHostException uh_ex){ + } + } + } + + /** + Writes the message to the stream. + @param out Output stream to which message should be written. + */ + public void write(OutputStream out)throws SocksException, + IOException{ + if(data == null){ + Socks5Message msg; + + if(addrType == SOCKS_ATYP_DOMAINNAME) + msg = new Socks5Message(command,host,port); + else{ + if(ip == null){ + try{ + ip = InetAddress.getByName(host); + }catch(UnknownHostException uh_ex){ + throw new SocksException(Proxy.SOCKS_JUST_ERROR); + } + } + msg = new Socks5Message(command,ip,port); + } + data = msg.data; + } + out.write(data); + } + + /** + Returns IP field of the message as IP, if the message was created + with ATYP of HOSTNAME, it will attempt to resolve the hostname, + which might fail. + @throws UnknownHostException if host can't be resolved. + */ + public InetAddress getInetAddress() throws UnknownHostException{ + if(ip!=null) return ip; + + return (ip=InetAddress.getByName(host)); + } + + /** + Returns string representation of the message. + */ + public String toString(){ + String s= + "Socks5Message:"+"\n"+ + "VN "+version+"\n"+ + "CMD "+command+"\n"+ + "ATYP "+addrType+"\n"+ + "ADDR "+host+"\n"+ + "PORT "+port+"\n"; + return s; + } + + + /** + *Wether to resolve hostIP returned from SOCKS server + *that is wether to create InetAddress object from the + *hostName string + */ + static public boolean resolveIP(){ return doResolveIP;} + + /** + *Wether to resolve hostIP returned from SOCKS server + *that is wether to create InetAddress object from the + *hostName string + *@param doResolve Wether to resolve hostIP from SOCKS server. + *@return Previous value. + */ + static public boolean resolveIP(boolean doResolve){ + boolean old = doResolveIP; + doResolveIP = doResolve; + return old; + } + + /* + private static final void debug(String s){ + if(DEBUG) + System.out.print(s); + } + private static final boolean DEBUG = false; + */ + + //SOCKS5 constants + public static final int SOCKS_VERSION =5; + + public static final int SOCKS_ATYP_IPV4 =0x1; //Where is 2?? + public static final int SOCKS_ATYP_DOMAINNAME =0x3; //!!!!rfc1928 + public static final int SOCKS_ATYP_IPV6 =0x4; + + public static final int SOCKS_IPV6_LENGTH =16; + + static boolean doResolveIP = true; + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/Socks5Proxy.java b/app/src/main/java/net/sourceforge/jsocks/Socks5Proxy.java new file mode 100644 index 0000000..aa9c643 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/Socks5Proxy.java @@ -0,0 +1,231 @@ +package net.sourceforge.jsocks; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.Hashtable; + +/** + SOCKS5 Proxy. +*/ + +public class Socks5Proxy extends Proxy implements Cloneable{ + +//Data members + private Hashtable authMethods = new Hashtable(); + private int selectedMethod; + + boolean resolveAddrLocally = true; + UDPEncapsulation udp_encapsulation=null; + + +//Public Constructors +//==================== + + /** + Creates SOCKS5 proxy. + @param proxyHost Host on which a Proxy server runs. + @param proxyPort Port on which a Proxy server listens for connections. + @throws UnknownHostException If proxyHost can't be resolved. + */ + public Socks5Proxy(String proxyHost,int proxyPort) + throws UnknownHostException{ + super(proxyHost,proxyPort); + version = 5; + setAuthenticationMethod(0,new AuthenticationNone()); + } + + + /** + Creates SOCKS5 proxy. + @param proxyIP Host on which a Proxy server runs. + @param proxyPort Port on which a Proxy server listens for connections. + */ + public Socks5Proxy(InetAddress proxyIP,int proxyPort){ + super(proxyIP,proxyPort); + version = 5; + setAuthenticationMethod(0,new AuthenticationNone()); + } + + +//Public instance methods +//======================== + + + /** + * Wether to resolve address locally or to let proxy do so. +

+ SOCKS5 protocol allows to send host names rather then IPs in the + requests, this option controls wether the hostnames should be send + to the proxy server as names, or should they be resolved locally. + @param doResolve Wether to perform resolution locally. + @return Previous settings. + */ + public boolean resolveAddrLocally(boolean doResolve){ + boolean old = resolveAddrLocally; + resolveAddrLocally = doResolve; + return old; + } + /** + Get current setting on how the addresses should be handled. + @return Current setting for address resolution. + @see Socks5Proxy#resolveAddrLocally(boolean doResolve) + */ + public boolean resolveAddrLocally(){ + return resolveAddrLocally; + } + + /** + Adds another authentication method. + @param methodId Authentication method id, see rfc1928 + @param method Implementation of Authentication + @see Authentication + */ + public boolean setAuthenticationMethod(int methodId, + Authentication method){ + if(methodId<0 || methodId > 255) + return false; + if(method == null){ + //Want to remove a particular method + return (authMethods.remove(new Integer(methodId)) != null); + }else{//Add the method, or rewrite old one + authMethods.put(new Integer(methodId),method); + } + return true; + } + + /** + Get authentication method, which corresponds to given method id + @param methodId Authentication method id. + @return Implementation for given method or null, if one was not set. + */ + public Authentication getAuthenticationMethod(int methodId){ + Object method = authMethods.get(new Integer(methodId)); + if(method == null) return null; + return (Authentication)method; + } + + /** + Creates a clone of this Proxy. + */ + @SuppressWarnings("unchecked") +public Object clone(){ + Socks5Proxy newProxy = new Socks5Proxy(proxyIP,proxyPort); + newProxy.authMethods = (Hashtable) this.authMethods.clone(); + newProxy.resolveAddrLocally = resolveAddrLocally; + newProxy.chainProxy = chainProxy; + return newProxy; + } + +//Public Static(Class) Methods +//============================== + + +//Protected Methods +//================= + + protected Proxy copy(){ + Socks5Proxy copy = new Socks5Proxy(proxyIP,proxyPort); + copy.authMethods = this.authMethods; //same Hash, no copy + copy.chainProxy = this.chainProxy; + copy.resolveAddrLocally = this.resolveAddrLocally; + return copy; + } + /** + * + * + */ + protected void startSession()throws SocksException{ + super.startSession(); + Authentication auth; + Socket ps = proxySocket; //The name is too long + + try{ + + byte nMethods = (byte) authMethods.size(); //Number of methods + + byte[] buf = new byte[2+nMethods]; //2 is for VER,NMETHODS + buf[0] = (byte) version; + buf[1] = nMethods; //Number of methods + int i=2; + + Enumeration ids = authMethods.keys(); + while(ids.hasMoreElements()) + buf[i++] = (byte)((Integer)ids.nextElement()).intValue(); + + out.write(buf); + out.flush(); + + int versionNumber = in.read(); + selectedMethod = in.read(); + + if(versionNumber < 0 || selectedMethod < 0){ + //EOF condition was reached + endSession(); + throw(new SocksException(SOCKS_PROXY_IO_ERROR, + "Connection to proxy lost.")); + } + if(versionNumber < version){ + //What should we do?? + } + if(selectedMethod == 0xFF){ //No method selected + ps.close(); + throw ( new SocksException(SOCKS_AUTH_NOT_SUPPORTED)); + } + + auth = getAuthenticationMethod(selectedMethod); + if(auth == null){ + //This shouldn't happen, unless method was removed by other + //thread, or the server stuffed up + throw(new SocksException(SOCKS_JUST_ERROR, + "Speciefied Authentication not found!")); + } + Object[] in_out = auth.doSocksAuthentication(selectedMethod,ps); + if(in_out == null){ + //Authentication failed by some reason + throw(new SocksException(SOCKS_AUTH_FAILURE)); + } + //Most authentication methods are expected to return + //simply the input/output streams associated with + //the socket. However if the auth. method requires + //some kind of encryption/decryption being done on the + //connection it should provide classes to handle I/O. + + in = (InputStream) in_out[0]; + out = (OutputStream) in_out[1]; + if(in_out.length > 2) + udp_encapsulation = (UDPEncapsulation) in_out[2]; + + }catch(SocksException s_ex){ + throw s_ex; + }catch(UnknownHostException uh_ex){ + throw(new SocksException(SOCKS_PROXY_NO_CONNECT)); + }catch(SocketException so_ex){ + throw(new SocksException(SOCKS_PROXY_NO_CONNECT)); + }catch(IOException io_ex){ + //System.err.println(io_ex); + throw(new SocksException(SOCKS_PROXY_IO_ERROR,""+io_ex)); + } + } + + protected ProxyMessage formMessage(int cmd,InetAddress ip,int port){ + return new Socks5Message(cmd,ip,port); + } + protected ProxyMessage formMessage(int cmd,String host,int port) + throws UnknownHostException{ + if(resolveAddrLocally) + return formMessage(cmd,InetAddress.getByName(host),port); + else + return new Socks5Message(cmd,host,port); + } + protected ProxyMessage formMessage(InputStream in) + throws SocksException, + IOException{ + return new Socks5Message(in); + } + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/SocksException.java b/app/src/main/java/net/sourceforge/jsocks/SocksException.java new file mode 100644 index 0000000..764587f --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/SocksException.java @@ -0,0 +1,80 @@ +package net.sourceforge.jsocks; + +/** + Exception thrown by various socks classes to indicate errors + with protocol or unsuccessful server responses. +*/ +public class SocksException extends java.io.IOException{ + private static final long serialVersionUID = 6141184566248512277L; + + /** + Construct a SocksException with given error code. +

+ Tries to look up message which corresponds to this error code. + @param errCode Error code for this exception. + */ + public SocksException(int errCode){ + this.errCode = errCode; + if((errCode >> 16) == 0){ + //Server reply error message + errString = errCode <= serverReplyMessage.length ? + serverReplyMessage[errCode] : + UNASSIGNED_ERROR_MESSAGE; + }else{ + //Local error + errCode = (errCode >> 16) -1; + errString = errCode <= localErrorMessage.length ? + localErrorMessage[errCode] : + UNASSIGNED_ERROR_MESSAGE; + } + } + /** + Constructs a SocksException with given error code and message. + @param errCode Error code. + @param errString Error Message. + */ + public SocksException(int errCode,String errString){ + this.errCode = errCode; + this.errString = errString; + } + /** + Get the error code associated with this exception. + @return Error code associated with this exception. + */ + public int getErrorCode(){ + return errCode; + } + /** + Get human readable representation of this exception. + @return String represntation of this exception. + */ + public String toString(){ + return errString; + } + + static final String UNASSIGNED_ERROR_MESSAGE = + "Unknown error message"; + static final String serverReplyMessage[] = { + "Succeeded", + "General SOCKS server failure", + "Connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported" }; + + static final String localErrorMessage[] ={ + "SOCKS server not specified", + "Unable to contact SOCKS server", + "IO error", + "None of Authentication methods are supported", + "Authentication failed", + "General SOCKS fault" }; + + String errString; + public int errCode; + +}//End of SocksException class + diff --git a/app/src/main/java/net/sourceforge/jsocks/SocksServerSocket.java b/app/src/main/java/net/sourceforge/jsocks/SocksServerSocket.java new file mode 100644 index 0000000..179e9c4 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/SocksServerSocket.java @@ -0,0 +1,164 @@ +package net.sourceforge.jsocks; + +import java.net.*; +import java.io.*; + +/** + SocksServerSocket allows to accept connections from one particular + host through the SOCKS4 or SOCKS5 proxy. +*/ +public class SocksServerSocket extends ServerSocket{ + //Data members + protected Proxy proxy; + protected String localHost; + protected InetAddress localIP; + protected int localPort; + + boolean doing_direct = false; + InetAddress remoteAddr; + + /** + *Creates ServerSocket capable of accepting one connection + *through the firewall, uses given proxy. + *@param host Host from which the connection should be recieved. + *@param port Port number of the primary connection. + */ + public SocksServerSocket(String host, int port) throws SocksException, + UnknownHostException, IOException { + + super(0); + remoteAddr = InetAddress.getByName(host); + doDirect(); + } + + /** + * Creates ServerSocket capable of accepting one connection + * through the firewall, uses default Proxy. + *@param ip Host from which the connection should be recieved. + *@param port Port number of the primary connection. + */ + public SocksServerSocket(InetAddress ip, int port) throws SocksException, + IOException{ + this(Proxy.defaultProxy,ip,port); + } + + /** + *Creates ServerSocket capable of accepting one connection + *through the firewall, uses given proxy. + *@param ip Host from which the connection should be recieved. + *@param port Port number of the primary connection. + */ + public SocksServerSocket(Proxy p, InetAddress ip, int port) + throws SocksException, IOException { + super(0); + + remoteAddr = ip; + doDirect(); + } + + + /** + * Accepts the incoming connection. + */ + public Socket accept() throws IOException{ + Socket s; + + if(!doing_direct){ + if(proxy == null) return null; + + ProxyMessage msg = proxy.accept(); + s = msg.ip == null? new SocksSocket(msg.host,msg.port,proxy) + : new SocksSocket(msg.ip,msg.port,proxy); + //Set timeout back to 0 + proxy.proxySocket.setSoTimeout(0); + }else{ //Direct Connection + + //Mimic the proxy behaviour, + //only accept connections from the speciefed host. + while(true){ + s = super.accept(); + if(s.getInetAddress().equals(remoteAddr)){ + //got the connection from the right host + //Close listenning socket. + break; + }else + s.close(); //Drop all connections from other hosts + } + + } + proxy = null; + //Return accepted socket + return s; + } + + /** + * Closes the connection to proxy if socket have not been accepted, if + * the direct connection is used, closes direct ServerSocket. If the + * client socket have been allready accepted, does nothing. + */ + public void close() throws IOException{ + super.close(); + if(proxy != null) proxy.endSession(); + proxy = null; + } + + /** + Get the name of the host proxy is using to listen for incoming + connection. +

+ Usefull when address is returned by proxy as the hostname. + @return the hostname of the address proxy is using to listen + for incoming connection. + */ + public String getHost(){ + return localHost; + } + + /** + * Get address assigned by proxy to listen for incomming + * connections, or the local machine address if doing direct + * connection. + */ + public InetAddress getInetAddress(){ + if(localIP == null){ + try{ + localIP = InetAddress.getByName(localHost); + }catch(UnknownHostException e){ + return null; + } + } + return localIP; + } + + /** + * Get port assigned by proxy to listen for incoming connections, or + the port chosen by local system, if accepting directly. + */ + public int getLocalPort(){ + return localPort; + } + + /** + Set Timeout. + + @param timeout Amount of time in milliseconds, accept should wait for + incoming connection before failing with exception. + Zero timeout implies infinity. + */ + public void setSoTimeout(int timeout) throws SocketException{ + super.setSoTimeout(timeout); + if(!doing_direct) proxy.proxySocket.setSoTimeout(timeout); + } + + +//Private Methods +////////////////// + + private void doDirect(){ + doing_direct = true; + localPort = super.getLocalPort(); + localIP = super.getInetAddress(); + localHost = localIP.getHostName(); + } + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/SocksSocket.java b/app/src/main/java/net/sourceforge/jsocks/SocksSocket.java new file mode 100644 index 0000000..cf9ff65 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/SocksSocket.java @@ -0,0 +1,291 @@ +package net.sourceforge.jsocks; + +import java.net.*; +import java.io.*; + +/** + * SocksSocket tryies to look very similar to normal Socket, + * while allowing connections through the SOCKS4 or 5 proxy. + * To use this class you will have to identify proxy you need + * to use, Proxy class allows you to set default proxy, which + * will be used by all Socks aware sockets. You can also create + * either Socks4Proxy or Socks5Proxy, and use them by passing to the + * appropriate constructors. + *

+ * Using Socks package can be as easy as that: + * + *


+ *
+ *     import Socks.*;
+ *     ....
+ *
+ *     try{
+ *        //Specify SOCKS5 proxy
+ *        Proxy.setDefaultProxy("socks-proxy",1080);
+ *
+ *        //OR you still use SOCKS4
+ *        //Code below uses SOCKS4 proxy
+ *        //Proxy.setDefaultProxy("socks-proxy",1080,userName);
+ *
+ *        Socket s = SocksSocket("some.host.of.mine",13);
+ *        readTimeFromSock(s);
+ *     }catch(SocksException sock_ex){
+ *        //Usually it will turn in more or less meaningfull message
+ *        System.err.println("SocksException:"+sock_ex);
+ *     }
+ *
+ * 
+ *

+ * However if the need exist for more control, like resolving addresses + * remotely, or using some non-trivial authentication schemes, it can be done. + */ + +public class SocksSocket extends Socket{ + //Data members + protected Proxy proxy; + protected String localHost, remoteHost; + protected InetAddress localIP, remoteIP; + protected int localPort,remotePort; + + private Socket directSock = null; + + /** + * Tryies to connect to given host and port + * using default proxy. If no default proxy speciefied + * it throws SocksException with error code SOCKS_NO_PROXY. + @param host Machine to connect to. + @param port Port to which to connect. + * @see SocksSocket#SocksSocket(Proxy,String,int) + * @see Socks5Proxy#resolveAddrLocally + */ + public SocksSocket(String host,int port) + throws SocksException,UnknownHostException{ + this(Proxy.defaultProxy,host,port); + } + /** + * Connects to host port using given proxy server. + @param p Proxy to use. + @param host Machine to connect to. + @param port Port to which to connect. + @throws UnknownHostException + If one of the following happens: +

    + +
  1. Proxy settings say that address should be resolved locally, but + this fails. +
  2. Proxy settings say that the host should be contacted directly but + host name can't be resolved. +
+ @throws SocksException + If one of the following happens: +
    +
  • Proxy is is null. +
  • Proxy settings say that the host should be contacted directly but + this fails. +
  • Socks Server can't be contacted. +
  • Authentication fails. +
  • Connection is not allowed by the SOCKS proxy. +
  • SOCKS proxy can't establish the connection. +
  • Any IO error occured. +
  • Any protocol error occured. +
+ @throws IOexception if anything is wrong with I/O. + @see Socks5Proxy#resolveAddrLocally + */ + public SocksSocket(Proxy p, String host, int port) throws SocksException, + UnknownHostException { + remoteHost = host; + remotePort = port; + remoteIP = InetAddress.getByName(host); + doDirect(); + } + + /** + Connects to given ip and port using given Proxy server. + @param p Proxy to use. + @param ip Machine to connect to. + @param port Port to which to connect. + + */ + public SocksSocket(InetAddress ip, int port) throws SocksException{ + this.remoteIP = ip; + this.remotePort = port; + this.remoteHost = ip.getHostName(); + doDirect(); + } + + + /** + * These 2 constructors are used by the SocksServerSocket. + * This socket simply overrides remoteHost, remotePort + */ + protected SocksSocket(String host,int port,Proxy proxy){ + this.remotePort = port; + this.proxy = proxy; + this.localIP = proxy.proxySocket.getLocalAddress(); + this.localPort = proxy.proxySocket.getLocalPort(); + this.remoteHost = host; + } + protected SocksSocket(InetAddress ip,int port,Proxy proxy){ + remoteIP = ip; + remotePort = port; + this.proxy = proxy; + this.localIP = proxy.proxySocket.getLocalAddress(); + this.localPort = proxy.proxySocket.getLocalPort(); + remoteHost = remoteIP.getHostName(); + } + + /** + * Same as Socket + */ + public void close() throws IOException{ + if(proxy!= null)proxy.endSession(); + proxy = null; + } + /** + * Same as Socket + */ + public InputStream getInputStream(){ + return proxy.in; + } + /** + * Same as Socket + */ + public OutputStream getOutputStream(){ + return proxy.out; + } + /** + * Same as Socket + */ + public int getPort(){ + return remotePort; + } + /** + * Returns remote host name, it is usefull in cases when addresses + * are resolved by proxy, and we can't create InetAddress object. + @return The name of the host this socket is connected to. + */ + public String getHost(){ + return remoteHost; + } + /** + * Get remote host as InetAddress object, might return null if + * addresses are resolved by proxy, and it is not possible to resolve + * it locally + @return Ip address of the host this socket is connected to, or null + if address was returned by the proxy as DOMAINNAME and can't be + resolved locally. + */ + public InetAddress getInetAddress(){ + if(remoteIP == null){ + try{ + remoteIP = InetAddress.getByName(remoteHost); + }catch(UnknownHostException e){ + return null; + } + } + return remoteIP; + } + + /** + * Get the port assigned by the proxy for the socket, not + * the port on locall machine as in Socket. + @return Port of the socket used on the proxy server. + */ + public int getLocalPort(){ + return localPort; + } + + /** + * Get address assigned by proxy to make a remote connection, + * it might be different from the host specified for the proxy. + * Can return null if socks server returned this address as hostname + * and it can't be resolved locally, use getLocalHost() then. + @return Address proxy is using to make a connection. + */ + public InetAddress getLocalAddress(){ + if(localIP == null){ + try{ + localIP = InetAddress.getByName(localHost); + }catch(UnknownHostException e){ + return null; + } + } + return localIP; + } + /** + Get name of the host, proxy has assigned to make a remote connection + for this socket. This method is usefull when proxy have returned + address as hostname, and we can't resolve it on this machine. + @return The name of the host proxy is using to make a connection. + */ + public String getLocalHost(){ + return localHost; + } + + /** + Same as socket. + */ + public void setSoLinger(boolean on,int val) throws SocketException{ + proxy.proxySocket.setSoLinger(on,val); + } + /** + Same as socket. + */ + public int getSoLinger(int timeout) throws SocketException{ + return proxy.proxySocket.getSoLinger(); + } + /** + Same as socket. + */ + public void setSoTimeout(int timeout) throws SocketException{ + proxy.proxySocket.setSoTimeout(timeout); + } + /** + Same as socket. + */ + public int getSoTimeout(int timeout) throws SocketException{ + return proxy.proxySocket.getSoTimeout(); + } + /** + Same as socket. + */ + public void setTcpNoDelay(boolean on) throws SocketException{ + proxy.proxySocket.setTcpNoDelay(on); + } + /** + Same as socket. + */ + public boolean getTcpNoDelay() throws SocketException{ + return proxy.proxySocket.getTcpNoDelay(); + } + + /** + Get string representation of the socket. + */ + public String toString(){ + if(directSock!=null) return "Direct connection:"+directSock; + return ("Proxy:"+proxy+";"+"addr:"+remoteHost+",port:"+remotePort + +",localport:"+localPort); + + } + +//Private Methods +////////////////// + + private void doDirect()throws SocksException{ + try{ + //System.out.println("IP:"+remoteIP+":"+remotePort); + directSock = new Socket(remoteIP,remotePort); + proxy.out = directSock.getOutputStream(); + proxy.in = directSock.getInputStream(); + proxy.proxySocket = directSock; + localIP = directSock.getLocalAddress(); + localPort = directSock.getLocalPort(); + }catch(IOException io_ex){ + throw new SocksException(Proxy.SOCKS_DIRECT_FAILED, + "Direct connect failed:"+io_ex); + } + } + +} diff --git a/app/src/main/java/net/sourceforge/jsocks/UDPEncapsulation.java b/app/src/main/java/net/sourceforge/jsocks/UDPEncapsulation.java new file mode 100644 index 0000000..e965942 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/UDPEncapsulation.java @@ -0,0 +1,29 @@ +package net.sourceforge.jsocks; +/** + This interface provides for datagram encapsulation for SOCKSv5 protocol. +

+ SOCKSv5 allows for datagrams to be encapsulated for purposes of integrity + and/or authenticity. How it should be done is aggreed during the + authentication stage, and is authentication dependent. This interface is + provided to allow this encapsulation. + @see Authentication +*/ +public interface UDPEncapsulation{ + + /** + This method should provide any authentication depended transformation + on datagrams being send from/to the client. + + @param data Datagram data (including any SOCKS related bytes), to be + encapsulated/decapsulated. + @param out Wether the data is being send out. If true method should + encapsulate/encrypt data, otherwise it should decapsulate/ + decrypt data. + @throw IOException if for some reason data can be transformed correctly. + @return Should return byte array containing data after transformation. + It is possible to return same array as input, if transformation + only involves bit mangling, and no additional data is being + added or removed. + */ + byte[] udpEncapsulate(byte[] data, boolean out) throws java.io.IOException; +} diff --git a/app/src/main/java/net/sourceforge/jsocks/UDPRelayServer.java b/app/src/main/java/net/sourceforge/jsocks/UDPRelayServer.java new file mode 100644 index 0000000..dfa6016 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/UDPRelayServer.java @@ -0,0 +1,212 @@ +package net.sourceforge.jsocks; +import net.sourceforge.jsocks.server.*; +import java.net.*; +import java.io.*; + +/** + UDP Relay server, used by ProxyServer to perform udp forwarding. +*/ +class UDPRelayServer implements Runnable{ + + + DatagramSocket client_sock; + DatagramSocket remote_sock; + + Socket controlConnection; + + int relayPort; + InetAddress relayIP; + + Thread pipe_thread1,pipe_thread2; + Thread master_thread; + + ServerAuthenticator auth; + + long lastReadTime; + + static PrintStream log = null; + static Proxy proxy = null; + static int datagramSize = 0xFFFF;//64K, a bit more than max udp size + static int iddleTimeout = 180000;//3 minutes + + + /** + Constructs UDP relay server to communicate with client + on given ip and port. + @param clientIP Address of the client from whom datagrams + will be recieved and to whom they will be forwarded. + @param clientPort Clients port. + @param master_thread Thread which will be interrupted, when + UDP relay server stoppes for some reason. + @param controlConnection Socket which will be closed, before + interrupting the master thread, it is introduced due to a bug + in windows JVM which does not throw InterruptedIOException in + threads which block in I/O operation. + */ + public UDPRelayServer(InetAddress clientIP,int clientPort, + Thread master_thread, + Socket controlConnection, + ServerAuthenticator auth) + throws IOException{ + this.master_thread = master_thread; + this.controlConnection = controlConnection; + this.auth = auth; + + client_sock = new Socks5DatagramSocket(true,auth.getUdpEncapsulation(), + clientIP,clientPort); + relayPort = client_sock.getLocalPort(); + relayIP = client_sock.getLocalAddress(); + + if(relayIP.getHostAddress().equals("0.0.0.0")) + relayIP = InetAddress.getLocalHost(); + + if(proxy == null) + remote_sock = new DatagramSocket(); + else + remote_sock = new Socks5DatagramSocket(proxy,0,null); + } + + +//Public methods +///////////////// + + + /** + Sets the timeout for UDPRelay server.
+ Zero timeout implies infinity.
+ Default timeout is 3 minutes. + */ + + static public void setTimeout(int timeout){ + iddleTimeout = timeout; + } + + + /** + Sets the size of the datagrams used in the UDPRelayServer.
+ Default size is 64K, a bit more than maximum possible size of the + datagram. + */ + static public void setDatagramSize(int size){ + datagramSize = size; + } + + /** + Port to which client should send datagram for association. + */ + public int getRelayPort(){ + return relayPort; + } + /** + IP address to which client should send datagrams for association. + */ + public InetAddress getRelayIP(){ + return relayIP; + } + + /** + Starts udp relay server. + Spawns two threads of execution and returns. + */ + public void start() throws IOException{ + remote_sock.setSoTimeout(iddleTimeout); + client_sock.setSoTimeout(iddleTimeout); + + log("Starting UDP relay server on "+relayIP+":"+relayPort); + log("Remote socket "+remote_sock.getLocalAddress()+":"+ + remote_sock.getLocalPort()); + + pipe_thread1 = new Thread(this,"pipe1"); + pipe_thread2 = new Thread(this,"pipe2"); + + lastReadTime = System.currentTimeMillis(); + + pipe_thread1.start(); + pipe_thread2.start(); + } + + /** + Stops Relay server. +

+ Does not close control connection, does not interrupt master_thread. + */ + public synchronized void stop(){ + master_thread = null; + controlConnection = null; + abort(); + } + +//Runnable interface +//////////////////// + public void run(){ + try{ + if(Thread.currentThread().getName().equals("pipe1")) + pipe(remote_sock,client_sock,false); + else + pipe(client_sock,remote_sock,true); + }catch(IOException ioe){ + }finally{ + abort(); + log("UDP Pipe thread "+Thread.currentThread().getName()+" stopped."); + } + + } + +//Private methods +///////////////// + private synchronized void abort(){ + if(pipe_thread1 == null) return; + + log("Aborting UDP Relay Server"); + + remote_sock.close(); + client_sock.close(); + + if(controlConnection != null) + try{ controlConnection.close();} catch(IOException ioe){} + + if(master_thread!=null) master_thread.interrupt(); + + pipe_thread1.interrupt(); + pipe_thread2.interrupt(); + + pipe_thread1 = null; + } + + + static private void log(String s){ + if(log != null){ + log.println(s); + log.flush(); + } + } + + private void pipe(DatagramSocket from,DatagramSocket to,boolean out) + throws IOException{ + byte[] data = new byte[datagramSize]; + DatagramPacket dp = new DatagramPacket(data,data.length); + + while(true){ + try{ + from.receive(dp); + lastReadTime = System.currentTimeMillis(); + + if(auth.checkRequest(dp,out)) + to.send(dp); + + }catch(UnknownHostException uhe){ + log("Dropping datagram for unknown host"); + }catch(InterruptedIOException iioe){ + //log("Interrupted: "+iioe); + //If we were interrupted by other thread. + if(iddleTimeout == 0) return; + + //If last datagram was received, long time ago, return. + long timeSinceRead = System.currentTimeMillis() - lastReadTime; + if(timeSinceRead >= iddleTimeout -100) //-100 for adjustment + return; + } + dp.setLength(data.length); + } + } +} diff --git a/app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticator.java b/app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticator.java new file mode 100644 index 0000000..cb7f0af --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticator.java @@ -0,0 +1,120 @@ +package net.sourceforge.jsocks.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.Socket; + +import net.sourceforge.jsocks.ProxyMessage; +import net.sourceforge.jsocks.UDPEncapsulation; + +/** + Classes implementing this interface should provide socks server with + authentication and authorization of users. +**/ +public interface ServerAuthenticator{ + + /** + This method is called when a new connection accepted by the server. +

+ At this point no data have been extracted from the connection. It is + responsibility of this method to ensure that the next byte in the + stream after this method have been called is the first byte of the + socks request message. For SOCKSv4 there is no authentication data and + the first byte in the stream is part of the request. With SOCKSv5 however + there is an authentication data first. It is expected that implementaions + will process this authentication data. +

+ If authentication was successful an instance of ServerAuthentication + should be returned, it later will be used by the server to perform + authorization and some other things. If authentication fails null should + be returned, or an exception may be thrown. + + @param s Accepted Socket. + @return An instance of ServerAuthenticator to be used for this connection + or null + */ + ServerAuthenticator startSession(Socket s) throws IOException; + + /** + This method should return input stream which should be used on the + accepted socket. +

+ SOCKSv5 allows to have multiple authentication methods, and these methods + might require some kind of transformations being made on the data. +

+ This method is called on the object returned from the startSession + function. + */ + InputStream getInputStream(); + /** + This method should return output stream to use to write to the accepted + socket. +

+ SOCKSv5 allows to have multiple authentication methods, and these methods + might require some kind of transformations being made on the data. +

+ This method is called on the object returned from the startSession + function. + */ + OutputStream getOutputStream(); + + /** + This method should return UDPEncapsulation, which should be used + on the datagrams being send in/out. +

+ If no transformation should be done on the datagrams, this method + should return null. +

+ This method is called on the object returned from the startSession + function. + */ + + UDPEncapsulation getUdpEncapsulation(); + + /** + This method is called when a request have been read. +

+ Implementation should decide wether to grant request or not. Returning + true implies granting the request, false means request should be rejected. +

+ This method is called on the object returned from the startSession + function. + @param msg Request message. + @return true to grant request, false to reject it. + */ + boolean checkRequest(ProxyMessage msg); + + /** + This method is called when datagram is received by the server. +

+ Implementaions should decide wether it should be forwarded or dropped. + It is expecteed that implementation will use datagram address and port + information to make a decision, as well as anything else. Address and + port of the datagram are always correspond to remote machine. It is + either destination or source address. If out is true address is destination + address, else it is a source address, address of the machine from which + datagram have been received for the client. +

+ Implementaions should return true if the datagram is to be forwarded, and + false if the datagram should be dropped. +

+ This method is called on the object returned from the startSession + function. + + @param out If true the datagram is being send out(from the client), + otherwise it is an incoming datagram. + @return True to forward datagram false drop it silently. + */ + boolean checkRequest(DatagramPacket dp, boolean out); + + /** + This method is called when session is completed. Either due to normal + termination or due to any error condition. +

+ This method is called on the object returned from the startSession + function. + */ + void endSession(); +} diff --git a/app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticatorNone.java b/app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticatorNone.java new file mode 100644 index 0000000..e4edbe7 --- /dev/null +++ b/app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticatorNone.java @@ -0,0 +1,169 @@ +package net.sourceforge.jsocks.server; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PushbackInputStream; +import java.net.Socket; + +import net.sourceforge.jsocks.ProxyMessage; +import net.sourceforge.jsocks.UDPEncapsulation; + +/** + An implementation of ServerAuthenticator, which does not do + any authentication. +

+ Warning!!
Should not be +used on machines which are not behind the firewall. +

+It is only provided to make implementing other authentication schemes +easier.
+For Example:

+   class MyAuth extends socks.server.ServerAuthenticator{
+    ...
+    public ServerAuthenticator startSession(java.net.Socket s){
+      if(!checkHost(s.getInetAddress()) return null;
+      return super.startSession(s);
+    }
+
+    boolean checkHost(java.net.Inetaddress addr){
+      boolean allow;
+      //Do it somehow
+      return allow;
+    }
+   }
+
+*/ +public class ServerAuthenticatorNone implements ServerAuthenticator{ + + static final byte[] socks5response = {5,0}; + + InputStream in; + OutputStream out; + + /** + Creates new instance of the ServerAuthenticatorNone. + */ + public ServerAuthenticatorNone(){ + this.in = null; + this.out = null; + } + /** + Constructs new ServerAuthenticatorNone object suitable for returning + from the startSession function. + @param in Input stream to return from getInputStream method. + @param out Output stream to return from getOutputStream method. + */ + public ServerAuthenticatorNone(InputStream in, OutputStream out){ + this.in = in; + this.out = out; + } + /** + Grants access to everyone.Removes authentication related bytes from + the stream, when a SOCKS5 connection is being made, selects an + authentication NONE. + */ + public ServerAuthenticator startSession(Socket s) + throws IOException{ + + PushbackInputStream in = new PushbackInputStream(s.getInputStream()); + OutputStream out = s.getOutputStream(); + + int version = in.read(); + if(version == 5){ + if(!selectSocks5Authentication(in,out,0)) + return null; + }else if(version == 4){ + //Else it is the request message allready, version 4 + in.unread(version); + }else + return null; + + + return new ServerAuthenticatorNone(in,out); + } + + /** + Get input stream. + @return Input stream speciefied in the constructor. + */ + public InputStream getInputStream(){ + return in; + } + /** + Get output stream. + @return Output stream speciefied in the constructor. + */ + public OutputStream getOutputStream(){ + return out; + } + /** + Allways returns null. + @return null + */ + public UDPEncapsulation getUdpEncapsulation(){ + return null; + } + + /** + Allways returns true. + */ + public boolean checkRequest(ProxyMessage msg){ + return true; + } + + /** + Allways returns true. + */ + public boolean checkRequest(java.net.DatagramPacket dp, boolean out){ + return true; + } + + /** + Does nothing. + */ + public void endSession(){ + } + + /** + Convinience routine for selecting SOCKSv5 authentication. +

+ This method reads in authentication methods that client supports, + checks wether it supports given method. If it does, the notification + method is written back to client, that this method have been chosen + for authentication. If given method was not found, authentication + failure message is send to client ([5,FF]). + @param in Input stream, version byte should be removed from the stream + before calling this method. + @param out Output stream. + @param methodId Method which should be selected. + @return true if methodId was found, false otherwise. + */ + static public boolean selectSocks5Authentication(InputStream in, + OutputStream out, + int methodId) + throws IOException{ + + int num_methods = in.read(); + if (num_methods <= 0) return false; + byte method_ids[] = new byte[num_methods]; + byte response[] = new byte[2]; + boolean found = false; + + response[0] = (byte) 5; //SOCKS version + response[1] = (byte) 0xFF; //Not found, we are pessimistic + + int bread = 0; //bytes read so far + while(bread < num_methods) + bread += in.read(method_ids,bread,num_methods-bread); + + for(int i=0;i= rem ? rem : cbRemaining; + byte[] bArr = bb.array(); + char[] cArr = cb.array(); + int bStart = bb.position(); + int cStart = cb.position(); + int i; + for(i=bStart; i= 26){ + int index = (int)in - 26; + cArr[cStart++] = (char)arr[index]; + }else { + cArr[cStart++] = (char)(in & 0xFF); + } + } + bb.position(i); + cb.position(cStart); + if(rem == cbRemaining && bb.hasRemaining()) return CoderResult.OVERFLOW; + } else { + while(bb.hasRemaining()){ + if( cbRemaining == 0 ) return CoderResult.OVERFLOW; + char in = (char)(bb.get() & 0xFF); + if(in >= 26){ + int index = (int)in - 26; + cb.put(arr[index]); + }else { + cb.put((char)(in & 0xFF)); + } + cbRemaining--; + } +/* + } +*/ + } + return CoderResult.UNDERFLOW; + } + + final static char[] arr = { + 0x001C,0x001B,0x007F,0x001D,0x001E,0x001F, + 0x0020,0x0021,0x0022,0x0023,0x0024,0x0025,0x0026,0x0027, + 0x0028,0x0029,0x002A,0x002B,0x002C,0x002D,0x002E,0x002F, + 0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037, + 0x0038,0x0039,0x003A,0x003B,0x003C,0x003D,0x003E,0x003F, + 0x0040,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047, + 0x0048,0x0049,0x004A,0x004B,0x004C,0x004D,0x004E,0x004F, + 0x0050,0x0051,0x0052,0x0053,0x0054,0x0055,0x0056,0x0057, + 0x0058,0x0059,0x005A,0x005B,0x005C,0x005D,0x005E,0x005F, + 0x0060,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067, + 0x0068,0x0069,0x006A,0x006B,0x006C,0x006D,0x006E,0x006F, + 0x0070,0x0071,0x0072,0x0073,0x0074,0x0075,0x0076,0x0077, + 0x0078,0x0079,0x007A,0x007B,0x007C,0x007D,0x007E,0x001A, + 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7, + 0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5, + 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9, + 0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192, + 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA, + 0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB, + 0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556, + 0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510, + 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F, + 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567, + 0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B, + 0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580, + 0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x03BC,0x03C4, + 0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229, + 0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248, + 0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0 + }; + } + + private static final class Encoder extends CharsetEncoder{ + private Encoder(Charset cs){ + super(cs, 1, 1); + } + + private native void nEncode(long outAddr, int absolutePos, char[] array, int arrPosition, int[] res); + + protected CoderResult encodeLoop(CharBuffer cb, ByteBuffer bb){ + int bbRemaining = bb.remaining(); +/* TODO: support direct byte buffers + if(CharsetProviderImpl.hasLoadedNatives() && bb.isDirect() && cb.hasRemaining() && cb.hasArray()){ + int toProceed = cb.remaining(); + int cbPos = cb.position(); + int bbPos = bb.position(); + boolean throwOverflow = false; + if( bbRemaining < toProceed ) { + toProceed = bbRemaining; + throwOverflow = true; + } + int[] res = {toProceed, 0}; + nEncode(AddressUtil.getDirectBufferAddress(bb), bbPos, cb.array(), cb.arrayOffset()+cbPos, res); + if( res[0] <= 0 ) { + bb.position(bbPos-res[0]); + cb.position(cbPos-res[0]); + if(res[1]!=0) { + if(res[1] < 0) + return CoderResult.malformedForLength(-res[1]); + else + return CoderResult.unmappableForLength(res[1]); + } + }else{ + bb.position(bbPos+res[0]); + cb.position(cbPos+res[0]); + if(throwOverflow) return CoderResult.OVERFLOW; + } + }else{ +*/ + if(bb.hasArray() && cb.hasArray()) { + byte[] byteArr = bb.array(); + char[] charArr = cb.array(); + int rem = cb.remaining(); + int byteArrStart = bb.position(); + rem = bbRemaining <= rem ? bbRemaining : rem; + int x; + for(x = cb.position(); x < cb.position()+rem; x++) { + char c = charArr[x]; + if(c > (char)0x25A0){ + if (c >= 0xD800 && c <= 0xDFFF) { + if(x+1 < cb.limit()) { + char c1 = charArr[x+1]; + if(c1 >= 0xD800 && c1 <= 0xDFFF) { + cb.position(x); bb.position(byteArrStart); + return CoderResult.unmappableForLength(2); + } + } else { + cb.position(x); bb.position(byteArrStart); + return CoderResult.UNDERFLOW; + } + cb.position(x); bb.position(byteArrStart); + return CoderResult.malformedForLength(1); + } + cb.position(x); bb.position(byteArrStart); + return CoderResult.unmappableForLength(1); + }else{ + if(c < 0x1A) { + byteArr[byteArrStart++] = (byte)c; + } else { + int index = (int)c >> 8; + index = encodeIndex[index]; + if(index < 0) { + cb.position(x); bb.position(byteArrStart); + return CoderResult.unmappableForLength(1); + } + index <<= 8; + index += (int)c & 0xFF; + if((byte)arr[index] != 0){ + byteArr[byteArrStart++] = (byte)arr[index]; + }else{ + cb.position(x); bb.position(byteArrStart); + return CoderResult.unmappableForLength(1); + } + } + } + } + cb.position(x); + bb.position(byteArrStart); + if(rem == bbRemaining && cb.hasRemaining()) { + return CoderResult.OVERFLOW; + } + } else { + while(cb.hasRemaining()){ + if( bbRemaining == 0 ) return CoderResult.OVERFLOW; + char c = cb.get(); + if(c > (char)0x25A0){ + if (c >= 0xD800 && c <= 0xDFFF) { + if(cb.hasRemaining()) { + char c1 = cb.get(); + if(c1 >= 0xD800 && c1 <= 0xDFFF) { + cb.position(cb.position()-2); + return CoderResult.unmappableForLength(2); + } else { + cb.position(cb.position()-1); + } + } else { + cb.position(cb.position()-1); + return CoderResult.UNDERFLOW; + } + cb.position(cb.position()-1); + return CoderResult.malformedForLength(1); + } + cb.position(cb.position()-1); + return CoderResult.unmappableForLength(1); + }else{ + if(c < 0x1A) { + bb.put((byte)c); + } else { + int index = (int)c >> 8; + index = encodeIndex[index]; + if(index < 0) { + cb.position(cb.position()-1); + return CoderResult.unmappableForLength(1); + } + index <<= 8; + index += (int)c & 0xFF; + if((byte)arr[index] != 0){ + bb.put((byte)arr[index]); + }else{ + cb.position(cb.position()-1); + return CoderResult.unmappableForLength(1); + } + } + bbRemaining--; + } + } +/* TODO: support direct byte buffers + } +*/ + } + return CoderResult.UNDERFLOW; + } + + final static char arr[] = { + + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x7F,0x1B,0x1A,0x1D,0x1E,0x1F, + 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F, + 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F, + 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F, + 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F, + 0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F, + 0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x1C, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xFF,0xAD,0x9B,0x9C,0x00,0x9D,0x00,0x00,0x00,0x00,0xA6,0xAE,0xAA,0x00,0x00,0x00, + 0xF8,0xF1,0xFD,0x00,0x00,0x00,0x00,0xFA,0x00,0x00,0xA7,0xAF,0xAC,0xAB,0x00,0xA8, + 0x00,0x00,0x00,0x00,0x8E,0x8F,0x92,0x80,0x00,0x90,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0xA5,0x00,0x00,0x00,0x00,0x99,0x00,0x00,0x00,0x00,0x00,0x9A,0x00,0x00,0xE1, + 0x85,0xA0,0x83,0x00,0x84,0x86,0x91,0x87,0x8A,0x82,0x88,0x89,0x8D,0xA1,0x8C,0x8B, + 0x00,0xA4,0x95,0xA2,0x93,0x00,0x94,0xF6,0x00,0x97,0xA3,0x96,0x81,0x00,0x00,0x98, + + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x9F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0xE2,0x00,0x00,0x00,0x00,0xE9,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0xE4,0x00,0x00,0xE8,0x00,0x00,0xEA,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0xE0,0x00,0x00,0xEB,0xEE,0x00,0x00,0x00,0x00,0x00,0x00,0xE6,0x00,0x00,0x00, + 0xE3,0x00,0x00,0xE5,0xE7,0x00,0xED,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFC, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF9,0xFB,0x00,0x00,0x00,0xEC,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xEF,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF7,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0xF0,0x00,0x00,0xF3,0xF2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xA9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xF4,0xF5,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + + 0xC4,0x00,0xB3,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xDA,0x00,0x00,0x00, + 0xBF,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xD9,0x00,0x00,0x00,0xC3,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xB4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC2,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0xC1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC5,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xCD,0xBA,0xD5,0xD6,0xC9,0xB8,0xB7,0xBB,0xD4,0xD3,0xC8,0xBE,0xBD,0xBC,0xC6,0xC7, + 0xCC,0xB5,0xB6,0xB9,0xD1,0xD2,0xCB,0xCF,0xD0,0xCA,0xD8,0xD7,0xCE,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xDF,0x00,0x00,0x00,0xDC,0x00,0x00,0x00,0xDB,0x00,0x00,0x00,0xDD,0x00,0x00,0x00, + 0xDE,0xB0,0xB1,0xB2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + }; + + final static int[] encodeIndex = { + 0,1,-1,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + 3,-1,4,5,-1,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 + }; + } +} diff --git a/app/src/main/java/org/connectbot/ActionBarWrapper.java b/app/src/main/java/org/connectbot/ActionBarWrapper.java new file mode 100644 index 0000000..0c7b65d --- /dev/null +++ b/app/src/main/java/org/connectbot/ActionBarWrapper.java @@ -0,0 +1,83 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import org.connectbot.util.PreferenceConstants; + +import android.app.Activity; +import android.app.ActionBar; + +public abstract class ActionBarWrapper { + public interface OnMenuVisibilityListener { + public void onMenuVisibilityChanged(boolean isVisible); + } + + public static ActionBarWrapper getActionBar(Activity activity) { + if (PreferenceConstants.PRE_HONEYCOMB) + return new DummyActionBar(); + else + return new RealActionBar(activity); + } + + public void hide() { + } + + public void show() { + } + + public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) { + } + + public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) { + } + + private static class DummyActionBar extends ActionBarWrapper { + } + + private static class RealActionBar extends ActionBarWrapper { + private final ActionBar actionBar; + + public RealActionBar(Activity activity) { + actionBar = activity.getActionBar(); + } + + @Override + public void hide() { + actionBar.hide(); + } + + @Override + public void show() { + actionBar.show(); + } + + @Override + public void addOnMenuVisibilityListener(final OnMenuVisibilityListener listener) { + actionBar.addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() { + public void onMenuVisibilityChanged(boolean isVisible) { + listener.onMenuVisibilityChanged(isVisible); + } + }); + } + + @Override + public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) { + actionBar.setDisplayHomeAsUpEnabled(showHomeAsUp); + } + } +} diff --git a/app/src/main/java/org/connectbot/ColorsActivity.java b/app/src/main/java/org/connectbot/ColorsActivity.java new file mode 100644 index 0000000..38336f7 --- /dev/null +++ b/app/src/main/java/org/connectbot/ColorsActivity.java @@ -0,0 +1,331 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.util.Arrays; +import java.util.List; + +import org.connectbot.util.Colors; +import org.connectbot.util.HostDatabase; +import org.connectbot.util.UberColorPickerDialog; +import org.connectbot.util.UberColorPickerDialog.OnColorChangedListener; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.MenuItem.OnMenuItemClickListener; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.Spinner; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; + +/** + * @author Kenny Root + * + */ +public class ColorsActivity extends Activity implements OnItemClickListener, OnColorChangedListener, OnItemSelectedListener { + private GridView mColorGrid; + private Spinner mFgSpinner; + private Spinner mBgSpinner; + + private int mColorScheme; + + private List mColorList; + private HostDatabase hostdb; + + private int mCurrentColor = 0; + + private int[] mDefaultColors; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.act_colors); + + this.setTitle(String.format("%s: %s", + getResources().getText(R.string.app_name), + getResources().getText(R.string.title_colors))); + + mColorScheme = HostDatabase.DEFAULT_COLOR_SCHEME; + + hostdb = new HostDatabase(this); + + mColorList = Arrays.asList(hostdb.getColorsForScheme(mColorScheme)); + mDefaultColors = hostdb.getDefaultColorsForScheme(mColorScheme); + + mColorGrid = (GridView) findViewById(R.id.color_grid); + mColorGrid.setAdapter(new ColorsAdapter(true)); + mColorGrid.setOnItemClickListener(this); + mColorGrid.setSelection(0); + + mFgSpinner = (Spinner) findViewById(R.id.fg); + mFgSpinner.setAdapter(new ColorsAdapter(false)); + mFgSpinner.setSelection(mDefaultColors[0]); + mFgSpinner.setOnItemSelectedListener(this); + + mBgSpinner = (Spinner) findViewById(R.id.bg); + mBgSpinner.setAdapter(new ColorsAdapter(false)); + mBgSpinner.setSelection(mDefaultColors[1]); + mBgSpinner.setOnItemSelectedListener(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (hostdb != null) { + hostdb.close(); + hostdb = null; + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (hostdb == null) + hostdb = new HostDatabase(this); + } + + private class ColorsAdapter extends BaseAdapter { + private boolean mSquareViews; + + public ColorsAdapter(boolean squareViews) { + mSquareViews = squareViews; + } + + public View getView(int position, View convertView, ViewGroup parent) { + ColorView c; + + if (convertView == null) { + c = new ColorView(ColorsActivity.this, mSquareViews); + } else { + c = (ColorView) convertView; + } + + c.setColor(mColorList.get(position)); + c.setNumber(position + 1); + + return c; + } + + public int getCount() { + return mColorList.size(); + } + + public Object getItem(int position) { + return mColorList.get(position); + } + + public long getItemId(int position) { + return position; + } + } + + private class ColorView extends View { + private boolean mSquare; + + private Paint mTextPaint; + private Paint mShadowPaint; + + // Things we paint + private int mBackgroundColor; + private String mText; + + private int mAscent; + private int mWidthCenter; + private int mHeightCenter; + + public ColorView(Context context, boolean square) { + super(context); + + mSquare = square; + + mTextPaint = new Paint(); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextSize(16); + mTextPaint.setColor(0xFFFFFFFF); + mTextPaint.setTextAlign(Paint.Align.CENTER); + + mShadowPaint = new Paint(mTextPaint); + mShadowPaint.setStyle(Paint.Style.STROKE); + mShadowPaint.setStrokeCap(Paint.Cap.ROUND); + mShadowPaint.setStrokeJoin(Paint.Join.ROUND); + mShadowPaint.setStrokeWidth(4f); + mShadowPaint.setColor(0xFF000000); + + setPadding(10, 10, 10, 10); + } + + public void setColor(int color) { + mBackgroundColor = color; + } + + public void setNumber(int number) { + mText = Integer.toString(number); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = measureWidth(widthMeasureSpec); + + int height; + if (mSquare) + height = width; + else + height = measureHeight(heightMeasureSpec); + + mAscent = (int) mTextPaint.ascent(); + mWidthCenter = width / 2; + mHeightCenter = height / 2 - mAscent / 2; + + setMeasuredDimension(width, height); + } + + private int measureWidth(int measureSpec) { + int result = 0; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + result = specSize; + } else { + // Measure the text + result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + + getPaddingRight(); + if (specMode == MeasureSpec.AT_MOST) { + // Respect AT_MOST value if that was what is called for by + // measureSpec + result = Math.min(result, specSize); + } + } + + return result; + } + + private int measureHeight(int measureSpec) { + int result = 0; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + mAscent = (int) mTextPaint.ascent(); + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + result = specSize; + } else { + // Measure the text (beware: ascent is a negative number) + result = (int) (-mAscent + mTextPaint.descent()) + + getPaddingTop() + getPaddingBottom(); + if (specMode == MeasureSpec.AT_MOST) { + // Respect AT_MOST value if that was what is called for by + // measureSpec + result = Math.min(result, specSize); + } + } + return result; + } + + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawColor(mBackgroundColor); + + canvas.drawText(mText, mWidthCenter, mHeightCenter, mShadowPaint); + canvas.drawText(mText, mWidthCenter, mHeightCenter, mTextPaint); + } + } + + private void editColor(int colorNumber) { + mCurrentColor = colorNumber; + new UberColorPickerDialog(this, this, mColorList.get(colorNumber)).show(); + } + + public void onItemClick(AdapterView parent, View view, int position, long id) { + editColor(position); + } + + public void onNothingSelected(AdapterView arg0) { } + + public void colorChanged(int value) { + hostdb.setGlobalColor(mCurrentColor, value); + mColorList.set(mCurrentColor, value); + mColorGrid.invalidateViews(); + } + + public void onItemSelected(AdapterView parent, View view, int position, + long id) { + boolean needUpdate = false; + if (parent == mFgSpinner) { + if (position != mDefaultColors[0]) { + mDefaultColors[0] = position; + needUpdate = true; + } + } else if (parent == mBgSpinner) { + if (position != mDefaultColors[1]) { + mDefaultColors[1] = position; + needUpdate = true; + } + } + + if (needUpdate) + hostdb.setDefaultColorsForScheme(mColorScheme, mDefaultColors[0], mDefaultColors[1]); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + MenuItem reset = menu.add(R.string.menu_colors_reset); + reset.setAlphabeticShortcut('r'); + reset.setNumericShortcut('1'); + reset.setIcon(android.R.drawable.ic_menu_revert); + reset.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem arg0) { + // Reset each individual color to defaults. + for (int i = 0; i < Colors.defaults.length; i++) { + if (mColorList.get(i) != Colors.defaults[i]) { + hostdb.setGlobalColor(i, Colors.defaults[i]); + mColorList.set(i, Colors.defaults[i]); + } + } + mColorGrid.invalidateViews(); + + // Reset the default FG/BG colors as well. + mFgSpinner.setSelection(HostDatabase.DEFAULT_FG_COLOR); + mBgSpinner.setSelection(HostDatabase.DEFAULT_BG_COLOR); + hostdb.setDefaultColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME, + HostDatabase.DEFAULT_FG_COLOR, HostDatabase.DEFAULT_BG_COLOR); + + return true; + } + }); + + return true; + } +} diff --git a/app/src/main/java/org/connectbot/ConsoleActivity.java b/app/src/main/java/org/connectbot/ConsoleActivity.java new file mode 100644 index 0000000..2359bea --- /dev/null +++ b/app/src/main/java/org/connectbot/ConsoleActivity.java @@ -0,0 +1,1175 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.lang.ref.WeakReference; +import java.util.List; + +import org.connectbot.bean.SelectionArea; +import org.connectbot.service.PromptHelper; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalKeyListener; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.PreferenceConstants; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.preference.PreferenceManager; +import android.text.ClipboardManager; +import android.util.Log; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.view.View.OnTouchListener; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; +import android.widget.AdapterView.OnItemClickListener; + +import de.mud.terminal.vt320; + +public class ConsoleActivity extends Activity { + public final static String TAG = "ConnectBot.ConsoleActivity"; + + protected static final int REQUEST_EDIT = 1; + + private static final int CLICK_TIME = 400; + private static final float MAX_CLICK_DISTANCE = 25f; + private static final int KEYBOARD_DISPLAY_TIME = 1500; + + // Direction to shift the ViewFlipper + private static final int SHIFT_LEFT = 0; + private static final int SHIFT_RIGHT = 1; + + protected ViewFlipper flip = null; + protected TerminalManager bound = null; + protected LayoutInflater inflater = null; + + private SharedPreferences prefs = null; + + // determines whether or not menuitem accelerators are bound + // otherwise they collide with an external keyboard's CTRL-char + private boolean hardKeyboard = false; + + protected Uri requested; + + protected ClipboardManager clipboard; + private RelativeLayout stringPromptGroup; + protected EditText stringPrompt; + private TextView stringPromptInstructions; + + private RelativeLayout booleanPromptGroup; + private TextView booleanPrompt; + private Button booleanYes, booleanNo; + + private RelativeLayout keyboardGroup; + private Runnable keyboardGroupHider; + + private TextView empty; + + private Animation slide_left_in, slide_left_out, slide_right_in, slide_right_out, fade_stay_hidden, fade_out_delayed; + + private Animation keyboard_fade_in, keyboard_fade_out; + private float lastX, lastY; + + private InputMethodManager inputManager; + + private MenuItem disconnect, copy, paste, portForward, resize, urlscan; + + protected TerminalBridge copySource = null; + private int lastTouchRow, lastTouchCol; + + private boolean forcedOrientation; + + private Handler handler = new Handler(); + + private ImageView mKeyboardButton; + + private ActionBarWrapper actionBar; + private boolean inActionBarMenu = false; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + bound = ((TerminalManager.TerminalBinder) service).getService(); + + // let manager know about our event handling services + bound.disconnectHandler = disconnectHandler; + + Log.d(TAG, String.format("Connected to TerminalManager and found bridges.size=%d", bound.bridges.size())); + + bound.setResizeAllowed(true); + + // clear out any existing bridges and record requested index + flip.removeAllViews(); + + final String requestedNickname = (requested != null) ? requested.getFragment() : null; + int requestedIndex = 0; + + TerminalBridge requestedBridge = bound.getConnectedBridge(requestedNickname); + + // If we didn't find the requested connection, try opening it + if (requestedNickname != null && requestedBridge == null) { + try { + Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s), so creating one now", requested.toString(), requestedNickname)); + requestedBridge = bound.openConnection(requested); + } catch(Exception e) { + Log.e(TAG, "Problem while trying to create new requested bridge from URI", e); + } + } + + // create views for all bridges on this service + for (TerminalBridge bridge : bound.bridges) { + + final int currentIndex = addNewTerminalView(bridge); + + // check to see if this bridge was requested + if (bridge == requestedBridge) + requestedIndex = currentIndex; + } + + setDisplayedTerminal(requestedIndex); + } + + public void onServiceDisconnected(ComponentName className) { + // tell each bridge to forget about our prompt handler + synchronized (bound.bridges) { + for(TerminalBridge bridge : bound.bridges) + bridge.promptHelper.setHandler(null); + } + + flip.removeAllViews(); + updateEmptyVisible(); + bound = null; + } + }; + + protected Handler promptHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // someone below us requested to display a prompt + updatePromptVisible(); + } + }; + + protected Handler disconnectHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "Someone sending HANDLE_DISCONNECT to parentHandler"); + + // someone below us requested to display a password dialog + // they are sending nickname and requested + TerminalBridge bridge = (TerminalBridge)msg.obj; + + if (bridge.isAwaitingClose()) + closeBridge(bridge); + } + }; + + /** + * @param bridge + */ + private void closeBridge(final TerminalBridge bridge) { + synchronized (flip) { + final int flipIndex = getFlipIndex(bridge); + + if (flipIndex >= 0) { + if (flip.getDisplayedChild() == flipIndex) { + shiftCurrentTerminal(SHIFT_LEFT); + } + flip.removeViewAt(flipIndex); + + /* TODO Remove this workaround when ViewFlipper is fixed to listen + * to view removals. Android Issue 1784 + */ + final int numChildren = flip.getChildCount(); + if (flip.getDisplayedChild() >= numChildren && + numChildren > 0) { + flip.setDisplayedChild(numChildren - 1); + } + + updateEmptyVisible(); + } + + // If we just closed the last bridge, go back to the previous activity. + if (flip.getChildCount() == 0) { + finish(); + } + } + } + + protected View findCurrentView(int id) { + View view = flip.getCurrentView(); + if(view == null) return null; + return view.findViewById(id); + } + + protected PromptHelper getCurrentPromptHelper() { + View view = findCurrentView(R.id.console_flip); + if(!(view instanceof TerminalView)) return null; + return ((TerminalView)view).bridge.promptHelper; + } + + protected void hideAllPrompts() { + stringPromptGroup.setVisibility(View.GONE); + booleanPromptGroup.setVisibility(View.GONE); + } + + private void showEmulatedKeys() { + keyboardGroup.startAnimation(keyboard_fade_in); + keyboardGroup.setVisibility(View.VISIBLE); + actionBar.show(); + + if (keyboardGroupHider != null) + handler.removeCallbacks(keyboardGroupHider); + keyboardGroupHider = new Runnable() { + public void run() { + if (keyboardGroup.getVisibility() == View.GONE || inActionBarMenu) + return; + + keyboardGroup.startAnimation(keyboard_fade_out); + keyboardGroup.setVisibility(View.GONE); + actionBar.hide(); + keyboardGroupHider = null; + } + }; + handler.postDelayed(keyboardGroupHider, KEYBOARD_DISPLAY_TIME); + } + + private void hideEmulatedKeys() { + if (keyboardGroupHider != null) + handler.removeCallbacks(keyboardGroupHider); + keyboardGroup.setVisibility(View.GONE); + actionBar.hide(); + } + + // more like configureLaxMode -- enable network IO on UI thread + private void configureStrictMode() { + try { + Class.forName("android.os.StrictMode"); + StrictModeSetup.run(); + } catch (ClassNotFoundException e) { + } + } + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + configureStrictMode(); + hardKeyboard = getResources().getConfiguration().keyboard == + Configuration.KEYBOARD_QWERTY; + + this.setContentView(R.layout.act_console); + + clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE); + prefs = PreferenceManager.getDefaultSharedPreferences(this); + + // hide status bar if requested by user + if (prefs.getBoolean(PreferenceConstants.FULLSCREEN, false)) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + // TODO find proper way to disable volume key beep if it exists. + setVolumeControlStream(AudioManager.STREAM_MUSIC); + + // handle requested console from incoming intent + requested = getIntent().getData(); + + inflater = LayoutInflater.from(this); + + flip = (ViewFlipper)findViewById(R.id.console_flip); + empty = (TextView)findViewById(android.R.id.empty); + + stringPromptGroup = (RelativeLayout) findViewById(R.id.console_password_group); + stringPromptInstructions = (TextView) findViewById(R.id.console_password_instructions); + stringPrompt = (EditText)findViewById(R.id.console_password); + stringPrompt.setOnKeyListener(new OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + if(event.getAction() == KeyEvent.ACTION_UP) return false; + if(keyCode != KeyEvent.KEYCODE_ENTER) return false; + + // pass collected password down to current terminal + String value = stringPrompt.getText().toString(); + + PromptHelper helper = getCurrentPromptHelper(); + if(helper == null) return false; + helper.setResponse(value); + + // finally clear password for next user + stringPrompt.setText(""); + updatePromptVisible(); + + return true; + } + }); + + booleanPromptGroup = (RelativeLayout) findViewById(R.id.console_boolean_group); + booleanPrompt = (TextView)findViewById(R.id.console_prompt); + + booleanYes = (Button)findViewById(R.id.console_prompt_yes); + booleanYes.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + PromptHelper helper = getCurrentPromptHelper(); + if(helper == null) return; + helper.setResponse(Boolean.TRUE); + updatePromptVisible(); + } + }); + + booleanNo = (Button)findViewById(R.id.console_prompt_no); + booleanNo.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + PromptHelper helper = getCurrentPromptHelper(); + if(helper == null) return; + helper.setResponse(Boolean.FALSE); + updatePromptVisible(); + } + }); + + // preload animations for terminal switching + slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in); + slide_left_out = AnimationUtils.loadAnimation(this, R.anim.slide_left_out); + slide_right_in = AnimationUtils.loadAnimation(this, R.anim.slide_right_in); + slide_right_out = AnimationUtils.loadAnimation(this, R.anim.slide_right_out); + + fade_out_delayed = AnimationUtils.loadAnimation(this, R.anim.fade_out_delayed); + fade_stay_hidden = AnimationUtils.loadAnimation(this, R.anim.fade_stay_hidden); + + // Preload animation for keyboard button + keyboard_fade_in = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_in); + keyboard_fade_out = AnimationUtils.loadAnimation(this, R.anim.keyboard_fade_out); + + inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + keyboardGroup = (RelativeLayout) findViewById(R.id.keyboard_group); + + mKeyboardButton = (ImageView) findViewById(R.id.button_keyboard); + mKeyboardButton.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + View flip = findCurrentView(R.id.console_flip); + if (flip == null) + return; + + inputManager.showSoftInput(flip, InputMethodManager.SHOW_FORCED); + hideEmulatedKeys(); + } + }); + + final ImageView ctrlButton = (ImageView) findViewById(R.id.button_ctrl); + ctrlButton.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + View flip = findCurrentView(R.id.console_flip); + if (flip == null) return; + TerminalView terminal = (TerminalView)flip; + + TerminalKeyListener handler = terminal.bridge.getKeyHandler(); + handler.metaPress(TerminalKeyListener.OUR_CTRL_ON); + hideEmulatedKeys(); + } + }); + + final ImageView escButton = (ImageView) findViewById(R.id.button_esc); + escButton.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + View flip = findCurrentView(R.id.console_flip); + if (flip == null) return; + TerminalView terminal = (TerminalView)flip; + + TerminalKeyListener handler = terminal.bridge.getKeyHandler(); + handler.sendEscape(); + hideEmulatedKeys(); + } + }); + + actionBar = ActionBarWrapper.getActionBar(this); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.hide(); + actionBar.addOnMenuVisibilityListener(new ActionBarWrapper.OnMenuVisibilityListener() { + public void onMenuVisibilityChanged(boolean isVisible) { + inActionBarMenu = isVisible; + if (isVisible == false) { + hideEmulatedKeys(); + } + } + }); + + // detect fling gestures to switch between terminals + final GestureDetector detect = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + private float totalY = 0; + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + + final float distx = e2.getRawX() - e1.getRawX(); + final float disty = e2.getRawY() - e1.getRawY(); + final int goalwidth = flip.getWidth() / 2; + + // need to slide across half of display to trigger console change + // make sure user kept a steady hand horizontally + if (Math.abs(disty) < (flip.getHeight() / 4)) { + if (distx > goalwidth) { + shiftCurrentTerminal(SHIFT_RIGHT); + return true; + } + + if (distx < -goalwidth) { + shiftCurrentTerminal(SHIFT_LEFT); + return true; + } + + } + + return false; + } + + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + // if copying, then ignore + if (copySource != null && copySource.isSelectingForCopy()) + return false; + + if (e1 == null || e2 == null) + return false; + + // if releasing then reset total scroll + if (e2.getAction() == MotionEvent.ACTION_UP) { + totalY = 0; + } + + // activate consider if within x tolerance + if (Math.abs(e1.getX() - e2.getX()) < ViewConfiguration.getTouchSlop() * 4) { + + View flip = findCurrentView(R.id.console_flip); + if(flip == null) return false; + TerminalView terminal = (TerminalView)flip; + + // estimate how many rows we have scrolled through + // accumulate distance that doesn't trigger immediate scroll + totalY += distanceY; + final int moved = (int)(totalY / terminal.bridge.charHeight); + + // consume as scrollback only if towards right half of screen + if (e2.getX() > flip.getWidth() / 2) { + if (moved != 0) { + int base = terminal.bridge.buffer.getWindowBase(); + terminal.bridge.buffer.setWindowBase(base + moved); + totalY = 0; + return true; + } + } else { + // otherwise consume as pgup/pgdown for every 5 lines + if (moved > 5) { + ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0); + terminal.bridge.tryKeyVibrate(); + totalY = 0; + return true; + } else if (moved < -5) { + ((vt320)terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0); + terminal.bridge.tryKeyVibrate(); + totalY = 0; + return true; + } + + } + + } + + return false; + } + + + }); + + flip.setLongClickable(true); + flip.setOnTouchListener(new OnTouchListener() { + + public boolean onTouch(View v, MotionEvent event) { + + // when copying, highlight the area + if (copySource != null && copySource.isSelectingForCopy()) { + int row = (int)Math.floor(event.getY() / copySource.charHeight); + int col = (int)Math.floor(event.getX() / copySource.charWidth); + + SelectionArea area = copySource.getSelectionArea(); + + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + // recording starting area + if (area.isSelectingOrigin()) { + area.setRow(row); + area.setColumn(col); + lastTouchRow = row; + lastTouchCol = col; + copySource.redraw(); + } + return true; + case MotionEvent.ACTION_MOVE: + /* ignore when user hasn't moved since last time so + * we can fine-tune with directional pad + */ + if (row == lastTouchRow && col == lastTouchCol) + return true; + + // if the user moves, start the selection for other corner + area.finishSelectingOrigin(); + + // update selected area + area.setRow(row); + area.setColumn(col); + lastTouchRow = row; + lastTouchCol = col; + copySource.redraw(); + return true; + case MotionEvent.ACTION_UP: + /* If they didn't move their finger, maybe they meant to + * select the rest of the text with the directional pad. + */ + if (area.getLeft() == area.getRight() && + area.getTop() == area.getBottom()) { + return true; + } + + // copy selected area to clipboard + String copiedText = area.copyFrom(copySource.buffer); + + clipboard.setText(copiedText); + Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_done, copiedText.length()), Toast.LENGTH_LONG).show(); + // fall through to clear state + + case MotionEvent.ACTION_CANCEL: + // make sure we clear any highlighted area + area.reset(); + copySource.setSelectingForCopy(false); + copySource.redraw(); + return true; + } + } + + Configuration config = getResources().getConfiguration(); + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + lastX = event.getX(); + lastY = event.getY(); + } else if (event.getAction() == MotionEvent.ACTION_UP + && keyboardGroup.getVisibility() == View.GONE + && event.getEventTime() - event.getDownTime() < CLICK_TIME + && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE + && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) { + showEmulatedKeys(); + } + + // pass any touch events back to detector + return detect.onTouchEvent(event); + } + + }); + + } + + /** + * + */ + private void configureOrientation() { + String rotateDefault; + if (getResources().getConfiguration().keyboard == Configuration.KEYBOARD_NOKEYS) + rotateDefault = PreferenceConstants.ROTATION_PORTRAIT; + else + rotateDefault = PreferenceConstants.ROTATION_LANDSCAPE; + + String rotate = prefs.getString(PreferenceConstants.ROTATION, rotateDefault); + if (PreferenceConstants.ROTATION_DEFAULT.equals(rotate)) + rotate = rotateDefault; + + // request a forced orientation if requested by user + if (PreferenceConstants.ROTATION_LANDSCAPE.equals(rotate)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + forcedOrientation = true; + } else if (PreferenceConstants.ROTATION_PORTRAIT.equals(rotate)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + forcedOrientation = true; + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + forcedOrientation = false; + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + View view = findCurrentView(R.id.console_flip); + final boolean activeTerminal = (view instanceof TerminalView); + boolean sessionOpen = false; + boolean disconnected = false; + boolean canForwardPorts = false; + + if (activeTerminal) { + TerminalBridge bridge = ((TerminalView) view).bridge; + sessionOpen = bridge.isSessionOpen(); + disconnected = bridge.isDisconnected(); + canForwardPorts = bridge.canFowardPorts(); + } + + menu.setQwertyMode(true); + + disconnect = menu.add(R.string.list_host_disconnect); + if (hardKeyboard) + disconnect.setAlphabeticShortcut('w'); + if (!sessionOpen && disconnected) + disconnect.setTitle(R.string.console_menu_close); + disconnect.setEnabled(activeTerminal); + disconnect.setIcon(android.R.drawable.ic_menu_close_clear_cancel); + disconnect.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // disconnect or close the currently visible session + TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); + TerminalBridge bridge = terminalView.bridge; + + bridge.dispatchDisconnect(true); + return true; + } + }); + + copy = menu.add(R.string.console_menu_copy); + if (hardKeyboard) + copy.setAlphabeticShortcut('c'); + copy.setIcon(android.R.drawable.ic_menu_set_as); + copy.setEnabled(activeTerminal); + copy.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // mark as copying and reset any previous bounds + TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); + copySource = terminalView.bridge; + + SelectionArea area = copySource.getSelectionArea(); + area.reset(); + area.setBounds(copySource.buffer.getColumns(), copySource.buffer.getRows()); + + copySource.setSelectingForCopy(true); + + // Make sure we show the initial selection + copySource.redraw(); + + Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show(); + return true; + } + }); + + paste = menu.add(R.string.console_menu_paste); + if (hardKeyboard) + paste.setAlphabeticShortcut('v'); + paste.setIcon(android.R.drawable.ic_menu_edit); + paste.setEnabled(clipboard.hasText() && sessionOpen); + paste.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // force insert of clipboard text into current console + TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); + TerminalBridge bridge = terminalView.bridge; + + // pull string from clipboard and generate all events to force down + String clip = clipboard.getText().toString(); + bridge.injectString(clip); + + return true; + } + }); + + portForward = menu.add(R.string.console_menu_portforwards); + if (hardKeyboard) + portForward.setAlphabeticShortcut('f'); + portForward.setIcon(android.R.drawable.ic_menu_manage); + portForward.setEnabled(sessionOpen && canForwardPorts); + portForward.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); + TerminalBridge bridge = terminalView.bridge; + + Intent intent = new Intent(ConsoleActivity.this, PortForwardListActivity.class); + intent.putExtra(Intent.EXTRA_TITLE, bridge.host.getId()); + ConsoleActivity.this.startActivityForResult(intent, REQUEST_EDIT); + return true; + } + }); + + urlscan = menu.add(R.string.console_menu_urlscan); + if (hardKeyboard) + urlscan.setAlphabeticShortcut('u'); + urlscan.setIcon(android.R.drawable.ic_menu_search); + urlscan.setEnabled(activeTerminal); + urlscan.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); + + List urls = terminalView.bridge.scanForURLs(); + + Dialog urlDialog = new Dialog(ConsoleActivity.this); + urlDialog.setTitle(R.string.console_menu_urlscan); + + ListView urlListView = new ListView(ConsoleActivity.this); + URLItemListener urlListener = new URLItemListener(ConsoleActivity.this); + urlListView.setOnItemClickListener(urlListener); + + urlListView.setAdapter(new ArrayAdapter(ConsoleActivity.this, android.R.layout.simple_list_item_1, urls)); + urlDialog.setContentView(urlListView); + urlDialog.show(); + + return true; + } + }); + + resize = menu.add(R.string.console_menu_resize); + if (hardKeyboard) + resize.setAlphabeticShortcut('s'); + resize.setIcon(android.R.drawable.ic_menu_crop); + resize.setEnabled(sessionOpen); + resize.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + final TerminalView terminalView = (TerminalView) findCurrentView(R.id.console_flip); + + final View resizeView = inflater.inflate(R.layout.dia_resize, null, false); + new AlertDialog.Builder(ConsoleActivity.this) + .setView(resizeView) + .setPositiveButton(R.string.button_resize, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + int width, height; + try { + width = Integer.parseInt(((EditText) resizeView + .findViewById(R.id.width)) + .getText().toString()); + height = Integer.parseInt(((EditText) resizeView + .findViewById(R.id.height)) + .getText().toString()); + } catch (NumberFormatException nfe) { + // TODO change this to a real dialog where we can + // make the input boxes turn red to indicate an error. + return; + } + + terminalView.forceSize(width, height); + } + }).setNegativeButton(android.R.string.cancel, null).create().show(); + + return true; + } + }); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + setVolumeControlStream(AudioManager.STREAM_NOTIFICATION); + + final View view = findCurrentView(R.id.console_flip); + boolean activeTerminal = (view instanceof TerminalView); + boolean sessionOpen = false; + boolean disconnected = false; + boolean canForwardPorts = false; + + if (activeTerminal) { + TerminalBridge bridge = ((TerminalView) view).bridge; + sessionOpen = bridge.isSessionOpen(); + disconnected = bridge.isDisconnected(); + canForwardPorts = bridge.canFowardPorts(); + } + + disconnect.setEnabled(activeTerminal); + if (sessionOpen || !disconnected) + disconnect.setTitle(R.string.list_host_disconnect); + else + disconnect.setTitle(R.string.console_menu_close); + copy.setEnabled(activeTerminal); + paste.setEnabled(clipboard.hasText() && sessionOpen); + portForward.setEnabled(sessionOpen && canForwardPorts); + urlscan.setEnabled(activeTerminal); + resize.setEnabled(sessionOpen); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, HostListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onOptionsMenuClosed(Menu menu) { + super.onOptionsMenuClosed(menu); + + setVolumeControlStream(AudioManager.STREAM_MUSIC); + } + + @Override + public void onStart() { + super.onStart(); + + // connect with manager service to find all bridges + // when connected it will insert all views + bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + } + + @Override + public void onPause() { + super.onPause(); + Log.d(TAG, "onPause called"); + + if (forcedOrientation && bound != null) + bound.setResizeAllowed(false); + } + + @Override + public void onResume() { + super.onResume(); + Log.d(TAG, "onResume called"); + + // Make sure we don't let the screen fall asleep. + // This also keeps the Wi-Fi chipset from disconnecting us. + if (prefs.getBoolean(PreferenceConstants.KEEP_ALIVE, true)) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + configureOrientation(); + + if (forcedOrientation && bound != null) + bound.setResizeAllowed(true); + } + + /* (non-Javadoc) + * @see android.app.Activity#onNewIntent(android.content.Intent) + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + Log.d(TAG, "onNewIntent called"); + + requested = intent.getData(); + + if (requested == null) { + Log.e(TAG, "Got null intent data in onNewIntent()"); + return; + } + + if (bound == null) { + Log.e(TAG, "We're not bound in onNewIntent()"); + return; + } + + TerminalBridge requestedBridge = bound.getConnectedBridge(requested.getFragment()); + int requestedIndex = 0; + + synchronized (flip) { + if (requestedBridge == null) { + // If we didn't find the requested connection, try opening it + + try { + Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s),"+ + "so creating one now", requested.toString(), requested.getFragment())); + requestedBridge = bound.openConnection(requested); + } catch(Exception e) { + Log.e(TAG, "Problem while trying to create new requested bridge from URI", e); + // TODO: We should display an error dialog here. + return; + } + + requestedIndex = addNewTerminalView(requestedBridge); + } else { + final int flipIndex = getFlipIndex(requestedBridge); + if (flipIndex > requestedIndex) { + requestedIndex = flipIndex; + } + } + + setDisplayedTerminal(requestedIndex); + } + } + + @Override + public void onStop() { + super.onStop(); + + unbindService(connection); + } + + protected void shiftCurrentTerminal(final int direction) { + View overlay; + synchronized (flip) { + boolean shouldAnimate = flip.getChildCount() > 1; + + // Only show animation if there is something else to go to. + if (shouldAnimate) { + // keep current overlay from popping up again + overlay = findCurrentView(R.id.terminal_overlay); + if (overlay != null) + overlay.startAnimation(fade_stay_hidden); + + if (direction == SHIFT_LEFT) { + flip.setInAnimation(slide_left_in); + flip.setOutAnimation(slide_left_out); + flip.showNext(); + } else if (direction == SHIFT_RIGHT) { + flip.setInAnimation(slide_right_in); + flip.setOutAnimation(slide_right_out); + flip.showPrevious(); + } + } + + ConsoleActivity.this.updateDefault(); + + if (shouldAnimate) { + // show overlay on new slide and start fade + overlay = findCurrentView(R.id.terminal_overlay); + if (overlay != null) + overlay.startAnimation(fade_out_delayed); + } + + updatePromptVisible(); + } + } + + /** + * Save the currently shown {@link TerminalView} as the default. This is + * saved back down into {@link TerminalManager} where we can read it again + * later. + */ + private void updateDefault() { + // update the current default terminal + View view = findCurrentView(R.id.console_flip); + if(!(view instanceof TerminalView)) return; + + TerminalView terminal = (TerminalView)view; + if(bound == null) return; + bound.defaultBridge = terminal.bridge; + } + + protected void updateEmptyVisible() { + // update visibility of empty status message + empty.setVisibility((flip.getChildCount() == 0) ? View.VISIBLE : View.GONE); + } + + /** + * Show any prompts requested by the currently visible {@link TerminalView}. + */ + protected void updatePromptVisible() { + // check if our currently-visible terminalbridge is requesting any prompt services + View view = findCurrentView(R.id.console_flip); + + // Hide all the prompts in case a prompt request was canceled + hideAllPrompts(); + + if(!(view instanceof TerminalView)) { + // we dont have an active view, so hide any prompts + return; + } + + PromptHelper prompt = ((TerminalView)view).bridge.promptHelper; + if(String.class.equals(prompt.promptRequested)) { + stringPromptGroup.setVisibility(View.VISIBLE); + + String instructions = prompt.promptInstructions; + if (instructions != null && instructions.length() > 0) { + stringPromptInstructions.setVisibility(View.VISIBLE); + stringPromptInstructions.setText(instructions); + } else + stringPromptInstructions.setVisibility(View.GONE); + stringPrompt.setText(""); + stringPrompt.setHint(prompt.promptHint); + stringPrompt.requestFocus(); + + } else if(Boolean.class.equals(prompt.promptRequested)) { + booleanPromptGroup.setVisibility(View.VISIBLE); + booleanPrompt.setText(prompt.promptHint); + booleanYes.requestFocus(); + + } else { + hideAllPrompts(); + view.requestFocus(); + } + } + + private class URLItemListener implements OnItemClickListener { + private WeakReference contextRef; + + URLItemListener(Context context) { + this.contextRef = new WeakReference(context); + } + + public void onItemClick(AdapterView arg0, View view, int position, long id) { + Context context = contextRef.get(); + + if (context == null) + return; + + try { + TextView urlView = (TextView) view; + + String url = urlView.getText().toString(); + if (url.indexOf("://") < 0) + url = "http://" + url; + + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "couldn't open URL", e); + // We should probably tell the user that we couldn't find a handler... + } + } + + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + Log.d(TAG, String.format("onConfigurationChanged; requestedOrientation=%d, newConfig.orientation=%d", getRequestedOrientation(), newConfig.orientation)); + if (bound != null) { + if (forcedOrientation && + (newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE && + getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) || + (newConfig.orientation != Configuration.ORIENTATION_PORTRAIT && + getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)) + bound.setResizeAllowed(false); + else + bound.setResizeAllowed(true); + + bound.hardKeyboardHidden = (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES); + + mKeyboardButton.setVisibility(bound.hardKeyboardHidden ? View.VISIBLE : View.GONE); + } + } + + /** + * Adds a new TerminalBridge to the current set of views in our ViewFlipper. + * + * @param bridge TerminalBridge to add to our ViewFlipper + * @return the child index of the new view in the ViewFlipper + */ + private int addNewTerminalView(TerminalBridge bridge) { + // let them know about our prompt handler services + bridge.promptHelper.setHandler(promptHandler); + + // inflate each terminal view + RelativeLayout view = (RelativeLayout)inflater.inflate(R.layout.item_terminal, flip, false); + + // set the terminal overlay text + TextView overlay = (TextView)view.findViewById(R.id.terminal_overlay); + overlay.setText(bridge.host.getNickname()); + + // and add our terminal view control, using index to place behind overlay + TerminalView terminal = new TerminalView(ConsoleActivity.this, bridge); + terminal.setId(R.id.console_flip); + view.addView(terminal, 0); + + synchronized (flip) { + // finally attach to the flipper + flip.addView(view); + return flip.getChildCount() - 1; + } + } + + private int getFlipIndex(TerminalBridge bridge) { + synchronized (flip) { + final int children = flip.getChildCount(); + for (int i = 0; i < children; i++) { + final View view = flip.getChildAt(i).findViewById(R.id.console_flip); + + if (view == null || !(view instanceof TerminalView)) { + // How did that happen? + continue; + } + + final TerminalView tv = (TerminalView) view; + + if (tv.bridge == bridge) { + return i; + } + } + } + + return -1; + } + + /** + * Displays the child in the ViewFlipper at the requestedIndex and updates the prompts. + * + * @param requestedIndex the index of the terminal view to display + */ + private void setDisplayedTerminal(int requestedIndex) { + synchronized (flip) { + try { + // show the requested bridge if found, also fade out overlay + flip.setDisplayedChild(requestedIndex); + flip.getCurrentView().findViewById(R.id.terminal_overlay) + .startAnimation(fade_out_delayed); + } catch (NullPointerException npe) { + Log.d(TAG, "View went away when we were about to display it", npe); + } + + updatePromptVisible(); + updateEmptyVisible(); + } + } +} diff --git a/app/src/main/java/org/connectbot/GeneratePubkeyActivity.java b/app/src/main/java/org/connectbot/GeneratePubkeyActivity.java new file mode 100644 index 0000000..3a438c5 --- /dev/null +++ b/app/src/main/java/org/connectbot/GeneratePubkeyActivity.java @@ -0,0 +1,353 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; + +import org.connectbot.bean.PubkeyBean; +import org.connectbot.util.EntropyDialog; +import org.connectbot.util.EntropyView; +import org.connectbot.util.OnEntropyGatheredListener; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.RadioGroup.OnCheckedChangeListener; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import com.trilead.ssh2.signature.ECDSASHA2Verify; + +public class GeneratePubkeyActivity extends Activity implements OnEntropyGatheredListener { + /** + * + */ + private static final int RSA_MINIMUM_BITS = 768; + + public final static String TAG = "ConnectBot.GeneratePubkeyActivity"; + + final static int DEFAULT_BITS = 1024; + + final static int[] ECDSA_SIZES = ECDSASHA2Verify.getCurveSizes(); + + final static int ECDSA_DEFAULT_BITS = ECDSA_SIZES[0]; + + private LayoutInflater inflater = null; + + private EditText nickname; + private RadioGroup keyTypeGroup; + private SeekBar bitsSlider; + private EditText bitsText; + private CheckBox unlockAtStartup; + private CheckBox confirmUse; + private Button save; + private Dialog entropyDialog; + private ProgressDialog progress; + + private EditText password1, password2; + + private String keyType = PubkeyDatabase.KEY_TYPE_RSA; + private int minBits = 768; + private int bits = DEFAULT_BITS; + + private byte[] entropy; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.act_generatepubkey); + + nickname = (EditText) findViewById(R.id.nickname); + + keyTypeGroup = (RadioGroup) findViewById(R.id.key_type); + + bitsText = (EditText) findViewById(R.id.bits); + bitsSlider = (SeekBar) findViewById(R.id.bits_slider); + + password1 = (EditText) findViewById(R.id.password1); + password2 = (EditText) findViewById(R.id.password2); + + unlockAtStartup = (CheckBox) findViewById(R.id.unlock_at_startup); + + confirmUse = (CheckBox) findViewById(R.id.confirm_use); + + save = (Button) findViewById(R.id.save); + + inflater = LayoutInflater.from(this); + + nickname.addTextChangedListener(textChecker); + password1.addTextChangedListener(textChecker); + password2.addTextChangedListener(textChecker); + + keyTypeGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + public void onCheckedChanged(RadioGroup group, int checkedId) { + if (checkedId == R.id.rsa) { + minBits = RSA_MINIMUM_BITS; + + bitsSlider.setEnabled(true); + bitsSlider.setProgress(DEFAULT_BITS - minBits); + + bitsText.setText(String.valueOf(DEFAULT_BITS)); + bitsText.setEnabled(true); + + keyType = PubkeyDatabase.KEY_TYPE_RSA; + } else if (checkedId == R.id.dsa) { + // DSA keys can only be 1024 bits + + bitsSlider.setEnabled(false); + bitsSlider.setProgress(DEFAULT_BITS - minBits); + + bitsText.setText(String.valueOf(DEFAULT_BITS)); + bitsText.setEnabled(false); + + keyType = PubkeyDatabase.KEY_TYPE_DSA; + } else if (checkedId == R.id.ec) { + minBits = ECDSA_DEFAULT_BITS; + + bitsSlider.setEnabled(true); + bitsSlider.setProgress(ECDSA_DEFAULT_BITS - minBits); + + bitsText.setText(String.valueOf(ECDSA_DEFAULT_BITS)); + bitsText.setEnabled(true); + + keyType = PubkeyDatabase.KEY_TYPE_EC; + } + } + }); + + bitsSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + if (PubkeyDatabase.KEY_TYPE_EC.equals(keyType)) { + bits = getClosestFieldSize(progress + minBits); + seekBar.setProgress(bits - minBits); + } else { + // Stay evenly divisible by 8 because it looks nicer to have + // 2048 than 2043 bits. + final int ourProgress = progress - (progress % 8); + bits = minBits + ourProgress; + } + + bitsText.setText(String.valueOf(bits)); + } + + public void onStartTrackingTouch(SeekBar seekBar) { + // We don't care about the start. + } + + public void onStopTrackingTouch(SeekBar seekBar) { + // We don't care about the stop. + } + }); + + bitsText.setOnFocusChangeListener(new OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus) { + final boolean isEc = PubkeyDatabase.KEY_TYPE_EC.equals(keyType); + try { + bits = Integer.parseInt(bitsText.getText().toString()); + if (bits < minBits) { + bits = minBits; + bitsText.setText(String.valueOf(bits)); + } + if (isEc) { + bits = getClosestFieldSize(bits); + } + } catch (NumberFormatException nfe) { + bits = isEc ? ECDSA_DEFAULT_BITS : DEFAULT_BITS; + bitsText.setText(String.valueOf(bits)); + } + + bitsSlider.setProgress(bits - minBits); + } + } + }); + + save.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + GeneratePubkeyActivity.this.save.setEnabled(false); + + GeneratePubkeyActivity.this.startEntropyGather(); + } + }); + + } + + private void checkEntries() { + boolean allowSave = true; + + if (!password1.getText().toString().equals(password2.getText().toString())) + allowSave = false; + + if (nickname.getText().length() == 0) + allowSave = false; + + save.setEnabled(allowSave); + } + + private void startEntropyGather() { + final View entropyView = inflater.inflate(R.layout.dia_gatherentropy, null, false); + ((EntropyView)entropyView.findViewById(R.id.entropy)).addOnEntropyGatheredListener(GeneratePubkeyActivity.this); + entropyDialog = new EntropyDialog(GeneratePubkeyActivity.this, entropyView); + entropyDialog.show(); + } + + public void onEntropyGathered(byte[] entropy) { + // For some reason the entropy dialog was aborted, exit activity + if (entropy == null) { + finish(); + return; + } + + this.entropy = entropy.clone(); + + int numSetBits = 0; + for (int i = 0; i < 20; i++) + numSetBits += measureNumberOfSetBits(this.entropy[i]); + + Log.d(TAG, "Entropy distribution=" + (int)(100.0 * numSetBits / 160.0) + "%"); + + Log.d(TAG, "entropy gathered; attemping to generate key..."); + startKeyGen(); + } + + private void startKeyGen() { + progress = new ProgressDialog(GeneratePubkeyActivity.this); + progress.setMessage(GeneratePubkeyActivity.this.getResources().getText(R.string.pubkey_generating)); + progress.setIndeterminate(true); + progress.setCancelable(false); + progress.show(); + + Thread keyGenThread = new Thread(mKeyGen); + keyGenThread.setName("KeyGen"); + keyGenThread.start(); + } + + final private Runnable mKeyGen = new Runnable() { + public void run() { + try { + boolean encrypted = false; + + SecureRandom random = new SecureRandom(); + + // Work around JVM bug + random.nextInt(); + random.setSeed(entropy); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(keyType); + + keyPairGen.initialize(bits, random); + + KeyPair pair = keyPairGen.generateKeyPair(); + PrivateKey priv = pair.getPrivate(); + PublicKey pub = pair.getPublic(); + + String secret = password1.getText().toString(); + if (secret.length() > 0) + encrypted = true; + + Log.d(TAG, "private: " + PubkeyUtils.formatKey(priv)); + Log.d(TAG, "public: " + PubkeyUtils.formatKey(pub)); + + PubkeyBean pubkey = new PubkeyBean(); + pubkey.setNickname(nickname.getText().toString()); + pubkey.setType(keyType); + pubkey.setPrivateKey(PubkeyUtils.getEncodedPrivate(priv, secret)); + pubkey.setPublicKey(pub.getEncoded()); + pubkey.setEncrypted(encrypted); + pubkey.setStartup(unlockAtStartup.isChecked()); + pubkey.setConfirmUse(confirmUse.isChecked()); + + PubkeyDatabase pubkeydb = new PubkeyDatabase(GeneratePubkeyActivity.this); + pubkeydb.savePubkey(pubkey); + pubkeydb.close(); + } catch (Exception e) { + Log.e(TAG, "Could not generate key pair"); + + e.printStackTrace(); + } + + GeneratePubkeyActivity.this.runOnUiThread(new Runnable() { + public void run() { + progress.dismiss(); + GeneratePubkeyActivity.this.finish(); + } + }); + } + + }; + + final private TextWatcher textChecker = new TextWatcher() { + public void afterTextChanged(Editable s) {} + + public void beforeTextChanged(CharSequence s, int start, int count, + int after) {} + + public void onTextChanged(CharSequence s, int start, int before, + int count) { + checkEntries(); + } + }; + + private int measureNumberOfSetBits(byte b) { + int numSetBits = 0; + + for (int i = 0; i < 8; i++) { + if ((b & 1) == 1) + numSetBits++; + b >>= 1; + } + + return numSetBits; + } + + private int getClosestFieldSize(int bits) { + int outBits = ECDSA_DEFAULT_BITS; + int distance = Math.abs(bits - ECDSA_DEFAULT_BITS); + + for (int i = 1; i < ECDSA_SIZES.length; i++) { + int thisDistance = Math.abs(bits - ECDSA_SIZES[i]); + if (thisDistance < distance) { + distance = thisDistance; + outBits = ECDSA_SIZES[i]; + } + } + return outBits; + } +} diff --git a/app/src/main/java/org/connectbot/HelpActivity.java b/app/src/main/java/org/connectbot/HelpActivity.java new file mode 100644 index 0000000..d82777d --- /dev/null +++ b/app/src/main/java/org/connectbot/HelpActivity.java @@ -0,0 +1,77 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.io.IOException; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.AssetManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.LinearLayout; + +/** + * @author Kenny Root + * + */ +public class HelpActivity extends Activity { + public final static String TAG = "ConnectBot.HelpActivity"; + + public final static String HELPDIR = "help"; + public final static String SUFFIX = ".html"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_help); + + this.setTitle(String.format("%s: %s", + getResources().getText(R.string.app_name), + getResources().getText(R.string.title_help))); + + AssetManager am = this.getAssets(); + LinearLayout content = (LinearLayout)this.findViewById(R.id.topics); + + try { + for (String name : am.list(HELPDIR)) { + if (name.endsWith(SUFFIX)) { + Button button = new Button(this); + final String topic = name.substring(0, name.length() - SUFFIX.length()); + button.setText(topic); + + button.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + Intent intent = new Intent(HelpActivity.this, HelpTopicActivity.class); + intent.putExtra(Intent.EXTRA_TITLE, topic); + HelpActivity.this.startActivity(intent); + } + }); + + content.addView(button); + } + } + } catch (IOException e) { + // TODO Auto-generated catch block + Log.e(TAG, "couldn't get list of help assets", e); + } + } +} diff --git a/app/src/main/java/org/connectbot/HelpTopicActivity.java b/app/src/main/java/org/connectbot/HelpTopicActivity.java new file mode 100644 index 0000000..a5fa8e0 --- /dev/null +++ b/app/src/main/java/org/connectbot/HelpTopicActivity.java @@ -0,0 +1,49 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import org.connectbot.util.HelpTopicView; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * @author Kenny Root + * + */ +public class HelpTopicActivity extends Activity { + public final static String TAG = "ConnectBot.HelpActivity"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_help_topic); + + String topic = getIntent().getStringExtra(Intent.EXTRA_TITLE); + + this.setTitle(String.format("%s: %s - %s", + getResources().getText(R.string.app_name), + getResources().getText(R.string.title_help), + topic)); + + HelpTopicView helpTopic = (HelpTopicView) findViewById(R.id.topic_text); + + helpTopic.setTopic(topic); + } +} diff --git a/app/src/main/java/org/connectbot/HostEditorActivity.java b/app/src/main/java/org/connectbot/HostEditorActivity.java new file mode 100644 index 0000000..4e8427f --- /dev/null +++ b/app/src/main/java/org/connectbot/HostEditorActivity.java @@ -0,0 +1,433 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.connectbot.bean.HostBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.HostDatabase; +import org.connectbot.util.PubkeyDatabase; + +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.Bundle; +import android.os.IBinder; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.util.Log; + +public class HostEditorActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener { + public class CursorPreferenceHack implements SharedPreferences { + protected final String table; + protected final long id; + + protected Map values = new HashMap(); +// protected Map pubkeys = new HashMap(); + + public CursorPreferenceHack(String table, long id) { + this.table = table; + this.id = id; + + cacheValues(); + } + + protected final void cacheValues() { + // fill a cursor and cache the values locally + // this makes sure we dont have any floating cursor to dispose later + + SQLiteDatabase db = hostdb.getReadableDatabase(); + Cursor cursor = db.query(table, null, "_id = ?", + new String[] { String.valueOf(id) }, null, null, null); + + if (cursor.moveToFirst()) { + for(int i = 0; i < cursor.getColumnCount(); i++) { + String key = cursor.getColumnName(i); + if(key.equals(HostDatabase.FIELD_HOST_HOSTKEY)) continue; + String value = cursor.getString(i); + values.put(key, value); + } + } + cursor.close(); + db.close(); + +// db = pubkeydb.getReadableDatabase(); +// cursor = db.query(PubkeyDatabase.TABLE_PUBKEYS, +// new String[] { "_id", PubkeyDatabase.FIELD_PUBKEY_NICKNAME }, +// null, null, null, null, null); +// +// if (cursor.moveToFirst()) { +// do { +// String pubkeyid = String.valueOf(cursor.getLong(0)); +// String value = cursor.getString(1); +// pubkeys.put(pubkeyid, value); +// } while (cursor.moveToNext()); +// } +// +// cursor.close(); +// db.close(); + } + + public boolean contains(String key) { + return values.containsKey(key); + } + + public class Editor implements SharedPreferences.Editor { + + private ContentValues update = new ContentValues(); + + public SharedPreferences.Editor clear() { + Log.d(this.getClass().toString(), "clear()"); + update = new ContentValues(); + return this; + } + + public boolean commit() { + //Log.d(this.getClass().toString(), "commit() changes back to database"); + SQLiteDatabase db = hostdb.getWritableDatabase(); + db.update(table, update, "_id = ?", new String[] { String.valueOf(id) }); + db.close(); + + // make sure we refresh the parent cached values + cacheValues(); + + // and update any listeners + for(OnSharedPreferenceChangeListener listener : listeners) { + listener.onSharedPreferenceChanged(CursorPreferenceHack.this, null); + } + + return true; + } + + // Gingerbread compatibility + public void apply() { + commit(); + } + + public android.content.SharedPreferences.Editor putBoolean(String key, boolean value) { + return this.putString(key, Boolean.toString(value)); + } + + public android.content.SharedPreferences.Editor putFloat(String key, float value) { + return this.putString(key, Float.toString(value)); + } + + public android.content.SharedPreferences.Editor putInt(String key, int value) { + return this.putString(key, Integer.toString(value)); + } + + public android.content.SharedPreferences.Editor putLong(String key, long value) { + return this.putString(key, Long.toString(value)); + } + + public android.content.SharedPreferences.Editor putString(String key, String value) { + //Log.d(this.getClass().toString(), String.format("Editor.putString(key=%s, value=%s)", key, value)); + update.put(key, value); + return this; + } + + public android.content.SharedPreferences.Editor remove(String key) { + //Log.d(this.getClass().toString(), String.format("Editor.remove(key=%s)", key)); + update.remove(key); + return this; + } + + public android.content.SharedPreferences.Editor putStringSet(String key, Set value) { + throw new UnsupportedOperationException("HostEditor Prefs do not support Set"); + } + } + + + public Editor edit() { + //Log.d(this.getClass().toString(), "edit()"); + return new Editor(); + } + + public Map getAll() { + return values; + } + + public boolean getBoolean(String key, boolean defValue) { + return Boolean.valueOf(this.getString(key, Boolean.toString(defValue))); + } + + public float getFloat(String key, float defValue) { + return Float.valueOf(this.getString(key, Float.toString(defValue))); + } + + public int getInt(String key, int defValue) { + return Integer.valueOf(this.getString(key, Integer.toString(defValue))); + } + + public long getLong(String key, long defValue) { + return Long.valueOf(this.getString(key, Long.toString(defValue))); + } + + public String getString(String key, String defValue) { + //Log.d(this.getClass().toString(), String.format("getString(key=%s, defValue=%s)", key, defValue)); + + if(!values.containsKey(key)) return defValue; + return values.get(key); + } + + public Set getStringSet(String key, Set defValue) { + throw new ClassCastException("HostEditor Prefs do not support Set"); + } + + protected List listeners = new LinkedList(); + + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + listeners.add(listener); + } + + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + listeners.remove(listener); + } + + } + + @Override + public SharedPreferences getSharedPreferences(String name, int mode) { + //Log.d(this.getClass().toString(), String.format("getSharedPreferences(name=%s)", name)); + return this.pref; + } + + protected static final String TAG = "ConnectBot.HostEditorActivity"; + + protected HostDatabase hostdb = null; + private PubkeyDatabase pubkeydb = null; + + private CursorPreferenceHack pref; + private ServiceConnection connection; + + private HostBean host; + protected TerminalBridge hostBridge; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + long hostId = this.getIntent().getLongExtra(Intent.EXTRA_TITLE, -1); + + // TODO: we could pass through a specific ContentProvider uri here + //this.getPreferenceManager().setSharedPreferencesName(uri); + + this.hostdb = new HostDatabase(this); + this.pubkeydb = new PubkeyDatabase(this); + + host = hostdb.findHostById(hostId); + + connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + TerminalManager bound = ((TerminalManager.TerminalBinder) service).getService(); + + hostBridge = bound.getConnectedBridge(host); + } + + public void onServiceDisconnected(ComponentName name) { + hostBridge = null; + } + }; + + this.pref = new CursorPreferenceHack(HostDatabase.TABLE_HOSTS, hostId); + this.pref.registerOnSharedPreferenceChangeListener(this); + + this.addPreferencesFromResource(R.xml.host_prefs); + + // add all existing pubkeys to our listpreference for user to choose from + // TODO: may be an issue here when this activity is recycled after adding a new pubkey + // TODO: should consider moving into onStart, but we dont have a good way of resetting the listpref after filling once + ListPreference pubkeyPref = (ListPreference)this.findPreference(HostDatabase.FIELD_HOST_PUBKEYID); + + List pubkeyNicks = new LinkedList(Arrays.asList(pubkeyPref.getEntries())); + pubkeyNicks.addAll(pubkeydb.allValues(PubkeyDatabase.FIELD_PUBKEY_NICKNAME)); + pubkeyPref.setEntries(pubkeyNicks.toArray(new CharSequence[pubkeyNicks.size()])); + + List pubkeyIds = new LinkedList(Arrays.asList(pubkeyPref.getEntryValues())); + pubkeyIds.addAll(pubkeydb.allValues("_id")); + pubkeyPref.setEntryValues(pubkeyIds.toArray(new CharSequence[pubkeyIds.size()])); + + // Populate the character set encoding list with all available + final ListPreference charsetPref = (ListPreference) findPreference(HostDatabase.FIELD_HOST_ENCODING); + + if (CharsetHolder.isInitialized()) { + initCharsetPref(charsetPref); + } else { + String[] currentCharsetPref = new String[1]; + currentCharsetPref[0] = charsetPref.getValue(); + charsetPref.setEntryValues(currentCharsetPref); + charsetPref.setEntries(currentCharsetPref); + + new Thread(new Runnable() { + public void run() { + initCharsetPref(charsetPref); + } + }).start(); + } + + this.updateSummaries(); + } + + @Override + public void onStart() { + super.onStart(); + + bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + if(this.hostdb == null) + this.hostdb = new HostDatabase(this); + + if(this.pubkeydb == null) + this.pubkeydb = new PubkeyDatabase(this); + } + + @Override + public void onStop() { + super.onStop(); + + unbindService(connection); + + if(this.hostdb != null) { + this.hostdb.close(); + this.hostdb = null; + } + + if(this.pubkeydb != null) { + this.pubkeydb.close(); + this.pubkeydb = null; + } + } + + private void updateSummaries() { + // for all text preferences, set hint as current database value + for (String key : this.pref.values.keySet()) { + if(key.equals(HostDatabase.FIELD_HOST_POSTLOGIN)) continue; + Preference pref = this.findPreference(key); + if(pref == null) continue; + if(pref instanceof CheckBoxPreference) continue; + CharSequence value = this.pref.getString(key, ""); + + if (key.equals(HostDatabase.FIELD_HOST_PUBKEYID)) { + try { + int pubkeyId = Integer.parseInt((String) value); + if (pubkeyId >= 0) + pref.setSummary(pubkeydb.getNickname(pubkeyId)); + else if(pubkeyId == HostDatabase.PUBKEYID_ANY) + pref.setSummary(R.string.list_pubkeyids_any); + else if(pubkeyId == HostDatabase.PUBKEYID_NEVER) + pref.setSummary(R.string.list_pubkeyids_none); + continue; + } catch (NumberFormatException nfe) { + // Fall through. + } + } else if (pref instanceof ListPreference) { + ListPreference listPref = (ListPreference) pref; + int entryIndex = listPref.findIndexOfValue((String) value); + if (entryIndex >= 0) + value = listPref.getEntries()[entryIndex]; + } + + pref.setSummary(value); + } + + } + + private void initCharsetPref(final ListPreference charsetPref) { + charsetPref.setEntryValues(CharsetHolder.getCharsetIds()); + charsetPref.setEntries(CharsetHolder.getCharsetNames()); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + // update values on changed preference + this.updateSummaries(); + + // Our CursorPreferenceHack always send null keys, so try to set charset anyway + if (hostBridge != null) + hostBridge.setCharset(sharedPreferences + .getString(HostDatabase.FIELD_HOST_ENCODING, HostDatabase.ENCODING_DEFAULT)); + } + + public static class CharsetHolder { + private static boolean initialized = false; + + private static CharSequence[] charsetIds; + private static CharSequence[] charsetNames; + + public static CharSequence[] getCharsetNames() { + if (charsetNames == null) + initialize(); + + return charsetNames; + } + + public static CharSequence[] getCharsetIds() { + if (charsetIds == null) + initialize(); + + return charsetIds; + } + + private synchronized static void initialize() { + if (initialized) + return; + + List charsetIdsList = new LinkedList(); + List charsetNamesList = new LinkedList(); + + for (Entry entry : Charset.availableCharsets().entrySet()) { + Charset c = entry.getValue(); + if (c.canEncode() && c.isRegistered()) { + String key = entry.getKey(); + if (key.startsWith("cp")) { + // Custom CP437 charset changes + charsetIdsList.add("CP437"); + charsetNamesList.add("CP437"); + } + charsetIdsList.add(entry.getKey()); + charsetNamesList.add(c.displayName()); + } + } + + charsetIds = charsetIdsList.toArray(new CharSequence[charsetIdsList.size()]); + charsetNames = charsetNamesList.toArray(new CharSequence[charsetNamesList.size()]); + + initialized = true; + } + + public static boolean isInitialized() { + return initialized; + } + } +} diff --git a/app/src/main/java/org/connectbot/HostListActivity.java b/app/src/main/java/org/connectbot/HostListActivity.java new file mode 100644 index 0000000..648b705 --- /dev/null +++ b/app/src/main/java/org/connectbot/HostListActivity.java @@ -0,0 +1,570 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.util.List; + +import org.connectbot.bean.HostBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.transport.TransportFactory; +import org.connectbot.util.HostDatabase; +import org.connectbot.util.PreferenceConstants; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.Intent.ShortcutIconResource; +import android.content.SharedPreferences.Editor; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.View.OnKeyListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.AdapterView.OnItemClickListener; + +public class HostListActivity extends ListActivity { + public final static int REQUEST_EDIT = 1; + + public final static int REQUEST_EULA = 2; + + protected TerminalManager bound = null; + + protected HostDatabase hostdb; + private List hosts; + protected LayoutInflater inflater = null; + + protected boolean sortedByColor = false; + + private MenuItem sortcolor; + + private MenuItem sortlast; + + private Spinner transportSpinner; + private TextView quickconnect; + + private SharedPreferences prefs = null; + + protected boolean makingShortcut = false; + + protected Handler updateHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + HostListActivity.this.updateList(); + } + }; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + bound = ((TerminalManager.TerminalBinder) service).getService(); + + // update our listview binder to find the service + HostListActivity.this.updateList(); + } + + public void onServiceDisconnected(ComponentName className) { + bound = null; + HostListActivity.this.updateList(); + } + }; + + @Override + public void onStart() { + super.onStart(); + + // start the terminal manager service + this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + if(this.hostdb == null) + this.hostdb = new HostDatabase(this); + } + + @Override + public void onStop() { + super.onStop(); + this.unbindService(connection); + + if(this.hostdb != null) { + this.hostdb.close(); + this.hostdb = null; + } + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_EULA) { + if(resultCode == Activity.RESULT_OK) { + // yay they agreed, so store that info + Editor edit = prefs.edit(); + edit.putBoolean(PreferenceConstants.EULA, true); + edit.commit(); + } else { + // user didnt agree, so close + this.finish(); + } + } else if (requestCode == REQUEST_EDIT) { + this.updateList(); + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_hostlist); + + this.setTitle(String.format("%s: %s", + getResources().getText(R.string.app_name), + getResources().getText(R.string.title_hosts_list))); + + this.prefs = PreferenceManager.getDefaultSharedPreferences(this); + + // detect HTC Dream and apply special preferences + if (Build.MANUFACTURER.equals("HTC") && Build.DEVICE.equals("dream")) { + if (!prefs.contains(PreferenceConstants.SHIFT_FKEYS) && + !prefs.contains(PreferenceConstants.CTRL_FKEYS)) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(PreferenceConstants.SHIFT_FKEYS, true); + editor.putBoolean(PreferenceConstants.CTRL_FKEYS, true); + editor.commit(); + } + } + + // check for eula agreement + boolean agreed = prefs.getBoolean(PreferenceConstants.EULA, false); + if(!agreed) { + this.startActivityForResult(new Intent(this, WizardActivity.class), REQUEST_EULA); + } + + this.makingShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction()) + || Intent.ACTION_PICK.equals(getIntent().getAction()); + + // connect with hosts database and populate list + this.hostdb = new HostDatabase(this); + ListView list = this.getListView(); + + this.sortedByColor = prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false); + + //this.list.setSelector(R.drawable.highlight_disabled_pressed); + + list.setOnItemClickListener(new OnItemClickListener() { + + public synchronized void onItemClick(AdapterView parent, View view, int position, long id) { + + // launch off to console details + HostBean host = (HostBean) parent.getAdapter().getItem(position); + Uri uri = host.getUri(); + + Intent contents = new Intent(Intent.ACTION_VIEW, uri); + contents.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + + if (makingShortcut) { + // create shortcut if requested + ShortcutIconResource icon = Intent.ShortcutIconResource.fromContext(HostListActivity.this, R.drawable.icon); + + Intent intent = new Intent(); + intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, contents); + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, host.getNickname()); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon); + + setResult(RESULT_OK, intent); + finish(); + + } else { + // otherwise just launch activity to show this host + HostListActivity.this.startActivity(contents); + } + } + }); + + this.registerForContextMenu(list); + + quickconnect = (TextView) this.findViewById(R.id.front_quickconnect); + quickconnect.setVisibility(makingShortcut ? View.GONE : View.VISIBLE); + quickconnect.setOnKeyListener(new OnKeyListener() { + + public boolean onKey(View v, int keyCode, KeyEvent event) { + + if(event.getAction() == KeyEvent.ACTION_UP) return false; + if(keyCode != KeyEvent.KEYCODE_ENTER) return false; + + return startConsoleActivity(); + } + }); + + transportSpinner = (Spinner)findViewById(R.id.transport_selection); + transportSpinner.setVisibility(makingShortcut ? View.GONE : View.VISIBLE); + ArrayAdapter transportSelection = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, TransportFactory.getTransportNames()); + transportSelection.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + transportSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView arg0, View view, int position, long id) { + String formatHint = TransportFactory.getFormatHint( + (String) transportSpinner.getSelectedItem(), + HostListActivity.this); + + quickconnect.setHint(formatHint); + quickconnect.setError(null); + quickconnect.requestFocus(); + } + public void onNothingSelected(AdapterView arg0) { } + }); + transportSpinner.setAdapter(transportSelection); + + this.inflater = LayoutInflater.from(this); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + // don't offer menus when creating shortcut + if (makingShortcut) return true; + + sortcolor.setVisible(!sortedByColor); + sortlast.setVisible(sortedByColor); + + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + // don't offer menus when creating shortcut + if(makingShortcut) return true; + + // add host, ssh keys, about + sortcolor = menu.add(R.string.list_menu_sortcolor); + sortcolor.setIcon(android.R.drawable.ic_menu_share); + sortcolor.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + sortedByColor = true; + updateList(); + return true; + } + }); + + sortlast = menu.add(R.string.list_menu_sortname); + sortlast.setIcon(android.R.drawable.ic_menu_share); + sortlast.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + sortedByColor = false; + updateList(); + return true; + } + }); + + MenuItem keys = menu.add(R.string.list_menu_pubkeys); + keys.setIcon(android.R.drawable.ic_lock_lock); + keys.setIntent(new Intent(HostListActivity.this, PubkeyListActivity.class)); + + MenuItem colors = menu.add(R.string.title_colors); + colors.setIcon(android.R.drawable.ic_menu_slideshow); + colors.setIntent(new Intent(HostListActivity.this, ColorsActivity.class)); + + MenuItem settings = menu.add(R.string.list_menu_settings); + settings.setIcon(android.R.drawable.ic_menu_preferences); + settings.setIntent(new Intent(HostListActivity.this, SettingsActivity.class)); + + MenuItem help = menu.add(R.string.title_help); + help.setIcon(android.R.drawable.ic_menu_help); + help.setIntent(new Intent(HostListActivity.this, HelpActivity.class)); + + return true; + + } + + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + + // create menu to handle hosts + + // create menu to handle deleting and sharing lists + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + final HostBean host = (HostBean) this.getListView().getItemAtPosition(info.position); + + menu.setHeaderTitle(host.getNickname()); + + // edit, disconnect, delete + MenuItem connect = menu.add(R.string.list_host_disconnect); + final TerminalBridge bridge = bound.getConnectedBridge(host); + connect.setEnabled((bridge != null)); + connect.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + bridge.dispatchDisconnect(true); + updateHandler.sendEmptyMessage(-1); + return true; + } + }); + + MenuItem edit = menu.add(R.string.list_host_edit); + edit.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent(HostListActivity.this, HostEditorActivity.class); + intent.putExtra(Intent.EXTRA_TITLE, host.getId()); + HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT); + return true; + } + }); + + MenuItem portForwards = menu.add(R.string.list_host_portforwards); + portForwards.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent(HostListActivity.this, PortForwardListActivity.class); + intent.putExtra(Intent.EXTRA_TITLE, host.getId()); + HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT); + return true; + } + }); + if (!TransportFactory.canForwardPorts(host.getProtocol())) + portForwards.setEnabled(false); + + MenuItem delete = menu.add(R.string.list_host_delete); + delete.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // prompt user to make sure they really want this + new AlertDialog.Builder(HostListActivity.this) + .setMessage(getString(R.string.delete_message, host.getNickname())) + .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // make sure we disconnect + if(bridge != null) + bridge.dispatchDisconnect(true); + + hostdb.deleteHost(host); + updateHandler.sendEmptyMessage(-1); + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + + return true; + } + }); + } + + /** + * @param text + * @return + */ + private boolean startConsoleActivity() { + Uri uri = TransportFactory.getUri((String) transportSpinner + .getSelectedItem(), quickconnect.getText().toString()); + + if (uri == null) { + quickconnect.setError(getString(R.string.list_format_error, + TransportFactory.getFormatHint( + (String) transportSpinner.getSelectedItem(), + HostListActivity.this))); + return false; + } + + HostBean host = TransportFactory.findHost(hostdb, uri); + if (host == null) { + host = TransportFactory.getTransport(uri.getScheme()).createHost(uri); + host.setColor(HostDatabase.COLOR_GRAY); + host.setPubkeyId(HostDatabase.PUBKEYID_ANY); + hostdb.saveHost(host); + } + + Intent intent = new Intent(HostListActivity.this, ConsoleActivity.class); + intent.setData(uri); + startActivity(intent); + + return true; + } + + protected void updateList() { + if (prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false) != sortedByColor) { + Editor edit = prefs.edit(); + edit.putBoolean(PreferenceConstants.SORT_BY_COLOR, sortedByColor); + edit.commit(); + } + + if (hostdb == null) + hostdb = new HostDatabase(this); + + hosts = hostdb.getHosts(sortedByColor); + + // Don't lose hosts that are connected via shortcuts but not in the database. + if (bound != null) { + for (TerminalBridge bridge : bound.bridges) { + if (!hosts.contains(bridge.host)) + hosts.add(0, bridge.host); + } + } + + HostAdapter adapter = new HostAdapter(this, hosts, bound); + + this.setListAdapter(adapter); + } + + class HostAdapter extends ArrayAdapter { + private List hosts; + private final TerminalManager manager; + private final ColorStateList red, green, blue; + + public final static int STATE_UNKNOWN = 1, STATE_CONNECTED = 2, STATE_DISCONNECTED = 3; + + class ViewHolder { + public TextView nickname; + public TextView caption; + public ImageView icon; + } + + public HostAdapter(Context context, List hosts, TerminalManager manager) { + super(context, R.layout.item_host, hosts); + + this.hosts = hosts; + this.manager = manager; + + red = context.getResources().getColorStateList(R.color.red); + green = context.getResources().getColorStateList(R.color.green); + blue = context.getResources().getColorStateList(R.color.blue); + } + + /** + * Check if we're connected to a terminal with the given host. + */ + private int getConnectedState(HostBean host) { + // always disconnected if we dont have backend service + if (this.manager == null) + return STATE_UNKNOWN; + + if (manager.getConnectedBridge(host) != null) + return STATE_CONNECTED; + + if (manager.disconnected.contains(host)) + return STATE_DISCONNECTED; + + return STATE_UNKNOWN; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_host, null, false); + + holder = new ViewHolder(); + + holder.nickname = (TextView)convertView.findViewById(android.R.id.text1); + holder.caption = (TextView)convertView.findViewById(android.R.id.text2); + holder.icon = (ImageView)convertView.findViewById(android.R.id.icon); + + convertView.setTag(holder); + } else + holder = (ViewHolder) convertView.getTag(); + + HostBean host = hosts.get(position); + if (host == null) { + // Well, something bad happened. We can't continue. + Log.e("HostAdapter", "Host bean is null!"); + + holder.nickname.setText("Error during lookup"); + holder.caption.setText("see 'adb logcat' for more"); + return convertView; + } + + holder.nickname.setText(host.getNickname()); + + switch (this.getConnectedState(host)) { + case STATE_UNKNOWN: + holder.icon.setImageState(new int[] { }, true); + break; + case STATE_CONNECTED: + holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true); + break; + case STATE_DISCONNECTED: + holder.icon.setImageState(new int[] { android.R.attr.state_expanded }, true); + break; + } + + ColorStateList chosen = null; + if (HostDatabase.COLOR_RED.equals(host.getColor())) + chosen = this.red; + else if (HostDatabase.COLOR_GREEN.equals(host.getColor())) + chosen = this.green; + else if (HostDatabase.COLOR_BLUE.equals(host.getColor())) + chosen = this.blue; + + Context context = convertView.getContext(); + + if (chosen != null) { + // set color normally if not selected + holder.nickname.setTextColor(chosen); + holder.caption.setTextColor(chosen); + } else { + // selected, so revert back to default black text + holder.nickname.setTextAppearance(context, android.R.attr.textAppearanceLarge); + holder.caption.setTextAppearance(context, android.R.attr.textAppearanceSmall); + } + + long now = System.currentTimeMillis() / 1000; + + String nice = context.getString(R.string.bind_never); + if (host.getLastConnect() > 0) { + int minutes = (int)((now - host.getLastConnect()) / 60); + if (minutes >= 60) { + int hours = (minutes / 60); + if (hours >= 24) { + int days = (hours / 24); + nice = context.getString(R.string.bind_days, days); + } else + nice = context.getString(R.string.bind_hours, hours); + } else + nice = context.getString(R.string.bind_minutes, minutes); + } + + holder.caption.setText(nice); + + return convertView; + } + } +} diff --git a/app/src/main/java/org/connectbot/PortForwardListActivity.java b/app/src/main/java/org/connectbot/PortForwardListActivity.java new file mode 100644 index 0000000..f9982e4 --- /dev/null +++ b/app/src/main/java/org/connectbot/PortForwardListActivity.java @@ -0,0 +1,425 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.util.List; + +import org.connectbot.bean.HostBean; +import org.connectbot.bean.PortForwardBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.HostDatabase; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.res.Resources; +import android.database.SQLException; +import android.graphics.Paint; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.MenuItem.OnMenuItemClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; + +/** + * List all portForwards for a particular host and provide a way for users to add more portForwards, + * edit existing portForwards, and delete portForwards. + * + * @author Kenny Root + */ +public class PortForwardListActivity extends ListActivity { + public final static String TAG = "ConnectBot.PortForwardListActivity"; + + private static final int LISTENER_CYCLE_TIME = 500; + + protected HostDatabase hostdb; + + private List portForwards; + + private ServiceConnection connection = null; + protected TerminalBridge hostBridge = null; + protected LayoutInflater inflater = null; + + private HostBean host; + + @Override + public void onStart() { + super.onStart(); + + this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + if(this.hostdb == null) + this.hostdb = new HostDatabase(this); + } + + @Override + public void onStop() { + super.onStop(); + + this.unbindService(connection); + + if(this.hostdb != null) { + this.hostdb.close(); + this.hostdb = null; + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + long hostId = this.getIntent().getLongExtra(Intent.EXTRA_TITLE, -1); + + setContentView(R.layout.act_portforwardlist); + + // connect with hosts database and populate list + this.hostdb = new HostDatabase(this); + host = hostdb.findHostById(hostId); + + { + final String nickname = host != null ? host.getNickname() : null; + final Resources resources = getResources(); + + if (nickname != null) { + this.setTitle(String.format("%s: %s (%s)", + resources.getText(R.string.app_name), + resources.getText(R.string.title_port_forwards_list), + nickname)); + } else { + this.setTitle(String.format("%s: %s", + resources.getText(R.string.app_name), + resources.getText(R.string.title_port_forwards_list))); + } + } + + connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + TerminalManager bound = ((TerminalManager.TerminalBinder) service).getService(); + + hostBridge = bound.getConnectedBridge(host); + updateHandler.sendEmptyMessage(-1); + } + + public void onServiceDisconnected(ComponentName name) { + hostBridge = null; + } + }; + + this.updateList(); + + this.registerForContextMenu(this.getListView()); + + this.getListView().setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView adapter, View view, int position, long id) { + ListView lv = PortForwardListActivity.this.getListView(); + PortForwardBean pfb = (PortForwardBean) lv.getItemAtPosition(position); + + if (hostBridge != null) { + if (pfb.isEnabled()) + hostBridge.disablePortForward(pfb); + else { + if (!hostBridge.enablePortForward(pfb)) + Toast.makeText(PortForwardListActivity.this, getString(R.string.portforward_problem), Toast.LENGTH_LONG).show(); + } + + updateHandler.sendEmptyMessage(-1); + } + } + }); + + this.inflater = LayoutInflater.from(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + MenuItem add = menu.add(R.string.portforward_menu_add); + add.setIcon(android.R.drawable.ic_menu_add); + add.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // build dialog to prompt user about updating + final View portForwardView = inflater.inflate(R.layout.dia_portforward, null, false); + final EditText destEdit = (EditText) portForwardView.findViewById(R.id.portforward_destination); + final Spinner typeSpinner = (Spinner)portForwardView.findViewById(R.id.portforward_type); + + typeSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { + public void onItemSelected(AdapterView value, View view, + int position, long id) { + destEdit.setEnabled(position != 2); + } + public void onNothingSelected(AdapterView arg0) { + } + }); + + new AlertDialog.Builder(PortForwardListActivity.this) + .setView(portForwardView) + .setPositiveButton(R.string.portforward_pos, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + try { + final EditText nicknameEdit = (EditText) portForwardView.findViewById(R.id.nickname); + final EditText sourcePortEdit = (EditText) portForwardView.findViewById(R.id.portforward_source); + + String type = HostDatabase.PORTFORWARD_LOCAL; + switch (typeSpinner.getSelectedItemPosition()) { + case 0: + type = HostDatabase.PORTFORWARD_LOCAL; + break; + case 1: + type = HostDatabase.PORTFORWARD_REMOTE; + break; + case 2: + type = HostDatabase.PORTFORWARD_DYNAMIC5; + break; + } + + PortForwardBean pfb = new PortForwardBean( + host != null ? host.getId() : -1, + nicknameEdit.getText().toString(), type, + sourcePortEdit.getText().toString(), + destEdit.getText().toString()); + + if (hostBridge != null) { + hostBridge.addPortForward(pfb); + hostBridge.enablePortForward(pfb); + } + + if (host != null && !hostdb.savePortForward(pfb)) + throw new SQLException("Could not save port forward"); + + updateHandler.sendEmptyMessage(-1); + } catch (Exception e) { + Log.e(TAG, "Could not update port forward", e); + // TODO Show failure dialog. + } + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + + return true; + } + }); + + return true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + // Create menu to handle deleting and editing port forward + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + final PortForwardBean pfb = (PortForwardBean) this.getListView().getItemAtPosition(info.position); + + menu.setHeaderTitle(pfb.getNickname()); + + MenuItem edit = menu.add(R.string.portforward_edit); + edit.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + final View editTunnelView = inflater.inflate(R.layout.dia_portforward, null, false); + + final Spinner typeSpinner = (Spinner) editTunnelView.findViewById(R.id.portforward_type); + if (HostDatabase.PORTFORWARD_LOCAL.equals(pfb.getType())) + typeSpinner.setSelection(0); + else if (HostDatabase.PORTFORWARD_REMOTE.equals(pfb.getType())) + typeSpinner.setSelection(1); + else + typeSpinner.setSelection(2); + + final EditText nicknameEdit = (EditText) editTunnelView.findViewById(R.id.nickname); + nicknameEdit.setText(pfb.getNickname()); + + final EditText sourcePortEdit = (EditText) editTunnelView.findViewById(R.id.portforward_source); + sourcePortEdit.setText(String.valueOf(pfb.getSourcePort())); + + final EditText destEdit = (EditText) editTunnelView.findViewById(R.id.portforward_destination); + if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(pfb.getType())) { + destEdit.setEnabled(false); + } else { + destEdit.setText(String.format("%s:%d", pfb.getDestAddr(), pfb.getDestPort())); + } + + typeSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { + public void onItemSelected(AdapterView value, View view, + int position, long id) { + destEdit.setEnabled(position != 2); + } + public void onNothingSelected(AdapterView arg0) { + } + }); + + new AlertDialog.Builder(PortForwardListActivity.this) + .setView(editTunnelView) + .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + try { + if (hostBridge != null) + hostBridge.disablePortForward(pfb); + + pfb.setNickname(nicknameEdit.getText().toString()); + + switch (typeSpinner.getSelectedItemPosition()) { + case 0: + pfb.setType(HostDatabase.PORTFORWARD_LOCAL); + break; + case 1: + pfb.setType(HostDatabase.PORTFORWARD_REMOTE); + break; + case 2: + pfb.setType(HostDatabase.PORTFORWARD_DYNAMIC5); + break; + } + + pfb.setSourcePort(Integer.parseInt(sourcePortEdit.getText().toString())); + pfb.setDest(destEdit.getText().toString()); + + // Use the new settings for the existing connection. + if (hostBridge != null) + updateHandler.postDelayed(new Runnable() { + public void run() { + hostBridge.enablePortForward(pfb); + updateHandler.sendEmptyMessage(-1); + } + }, LISTENER_CYCLE_TIME); + + + if (!hostdb.savePortForward(pfb)) + throw new SQLException("Could not save port forward"); + + updateHandler.sendEmptyMessage(-1); + } catch (Exception e) { + Log.e(TAG, "Could not update port forward", e); + // TODO Show failure dialog. + } + } + }) + .setNegativeButton(android.R.string.cancel, null).create().show(); + + return true; + } + }); + + MenuItem delete = menu.add(R.string.portforward_delete); + delete.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // prompt user to make sure they really want this + new AlertDialog.Builder(PortForwardListActivity.this) + .setMessage(getString(R.string.delete_message, pfb.getNickname())) + .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + try { + // Delete the port forward from the host if needed. + if (hostBridge != null) + hostBridge.removePortForward(pfb); + + hostdb.deletePortForward(pfb); + } catch (Exception e) { + Log.e(TAG, "Could not delete port forward", e); + } + + updateHandler.sendEmptyMessage(-1); + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + + return true; + } + }); + } + + protected Handler updateHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + PortForwardListActivity.this.updateList(); + } + }; + + protected void updateList() { + if (hostBridge != null) { + this.portForwards = hostBridge.getPortForwards(); + } else { + if (this.hostdb == null) return; + this.portForwards = this.hostdb.getPortForwardsForHost(host); + } + + PortForwardAdapter adapter = new PortForwardAdapter(this, portForwards); + + this.setListAdapter(adapter); + } + + class PortForwardAdapter extends ArrayAdapter { + class ViewHolder { + public TextView nickname; + public TextView caption; + } + + private List portForwards; + + public PortForwardAdapter(Context context, List portForwards) { + super(context, R.layout.item_portforward, portForwards); + + this.portForwards = portForwards; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_portforward, null, false); + + holder = new ViewHolder(); + holder.nickname = (TextView)convertView.findViewById(android.R.id.text1); + holder.caption = (TextView)convertView.findViewById(android.R.id.text2); + + convertView.setTag(holder); + } else + holder = (ViewHolder) convertView.getTag(); + + PortForwardBean pfb = portForwards.get(position); + holder.nickname.setText(pfb.getNickname()); + holder.caption.setText(pfb.getDescription()); + + if (hostBridge != null && !pfb.isEnabled()) { + holder.nickname.setPaintFlags(holder.nickname.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + holder.caption.setPaintFlags(holder.caption.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + + return convertView; + } + } +} diff --git a/app/src/main/java/org/connectbot/PubkeyListActivity.java b/app/src/main/java/org/connectbot/PubkeyListActivity.java new file mode 100644 index 0000000..be7a46f --- /dev/null +++ b/app/src/main/java/org/connectbot/PubkeyListActivity.java @@ -0,0 +1,673 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collections; +import java.util.EventListener; +import java.util.LinkedList; +import java.util.List; + +import org.connectbot.bean.PubkeyBean; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; +import org.openintents.intents.FileManagerIntents; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.IBinder; +import android.text.ClipboardManager; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TableRow; +import android.widget.TextView; +import android.widget.Toast; + +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.crypto.PEMStructure; + +/** + * List public keys in database by nickname and describe their properties. Allow users to import, + * generate, rename, and delete key pairs. + * + * @author Kenny Root + */ +public class PubkeyListActivity extends ListActivity implements EventListener { + public final static String TAG = "ConnectBot.PubkeyListActivity"; + + private static final int MAX_KEYFILE_SIZE = 8192; + private static final int REQUEST_CODE_PICK_FILE = 1; + + // Constants for AndExplorer's file picking intent + private static final String ANDEXPLORER_TITLE = "explorer_title"; + private static final String MIME_TYPE_ANDEXPLORER_FILE = "vnd.android.cursor.dir/lysesoft.andexplorer.file"; + + protected PubkeyDatabase pubkeydb; + private List pubkeys; + + protected ClipboardManager clipboard; + + protected LayoutInflater inflater = null; + + protected TerminalManager bound = null; + + private MenuItem onstartToggle = null; + private MenuItem confirmUse = null; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + bound = ((TerminalManager.TerminalBinder) service).getService(); + + // update our listview binder to find the service + updateList(); + } + + public void onServiceDisconnected(ComponentName className) { + bound = null; + updateList(); + } + }; + + @Override + public void onStart() { + super.onStart(); + + bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE); + + if(pubkeydb == null) + pubkeydb = new PubkeyDatabase(this); + } + + @Override + public void onStop() { + super.onStop(); + + unbindService(connection); + + if(pubkeydb != null) { + pubkeydb.close(); + pubkeydb = null; + } + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.act_pubkeylist); + + this.setTitle(String.format("%s: %s", + getResources().getText(R.string.app_name), + getResources().getText(R.string.title_pubkey_list))); + + // connect with hosts database and populate list + pubkeydb = new PubkeyDatabase(this); + + updateList(); + + registerForContextMenu(getListView()); + + getListView().setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView adapter, View view, int position, long id) { + PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(position); + boolean loaded = bound.isKeyLoaded(pubkey.getNickname()); + + // handle toggling key in-memory on/off + if(loaded) { + bound.removeKey(pubkey.getNickname()); + updateList(); + } else { + handleAddKey(pubkey); + } + + } + }); + + clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE); + + inflater = LayoutInflater.from(this); + } + + /** + * Read given file into memory as byte[]. + */ + protected static byte[] readRaw(File file) throws Exception { + InputStream is = new FileInputStream(file); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + int bytesRead; + byte[] buffer = new byte[1024]; + while ((bytesRead = is.read(buffer)) != -1) { + os.write(buffer, 0, bytesRead); + } + + os.flush(); + os.close(); + is.close(); + + return os.toByteArray(); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + MenuItem generatekey = menu.add(R.string.pubkey_generate); + generatekey.setIcon(android.R.drawable.ic_menu_manage); + generatekey.setIntent(new Intent(PubkeyListActivity.this, GeneratePubkeyActivity.class)); + + MenuItem importkey = menu.add(R.string.pubkey_import); + importkey.setIcon(android.R.drawable.ic_menu_upload); + importkey.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + Uri sdcard = Uri.fromFile(Environment.getExternalStorageDirectory()); + String pickerTitle = getString(R.string.pubkey_list_pick); + + // Try to use OpenIntent's file browser to pick a file + Intent intent = new Intent(FileManagerIntents.ACTION_PICK_FILE); + intent.setData(sdcard); + intent.putExtra(FileManagerIntents.EXTRA_TITLE, pickerTitle); + intent.putExtra(FileManagerIntents.EXTRA_BUTTON_TEXT, getString(android.R.string.ok)); + + try { + startActivityForResult(intent, REQUEST_CODE_PICK_FILE); + } catch (ActivityNotFoundException e) { + // If OI didn't work, try AndExplorer + intent = new Intent(Intent.ACTION_PICK); + intent.setDataAndType(sdcard, MIME_TYPE_ANDEXPLORER_FILE); + intent.putExtra(ANDEXPLORER_TITLE, pickerTitle); + + try { + startActivityForResult(intent, REQUEST_CODE_PICK_FILE); + } catch (ActivityNotFoundException e1) { + pickFileSimple(); + } + } + + return true; + } + }); + + return true; + } + + protected void handleAddKey(final PubkeyBean pubkey) { + if (pubkey.isEncrypted()) { + final View view = inflater.inflate(R.layout.dia_password, null); + final EditText passwordField = (EditText)view.findViewById(android.R.id.text1); + + new AlertDialog.Builder(PubkeyListActivity.this) + .setView(view) + .setPositiveButton(R.string.pubkey_unlock, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + handleAddKey(pubkey, passwordField.getText().toString()); + } + }) + .setNegativeButton(android.R.string.cancel, null).create().show(); + } else { + handleAddKey(pubkey, null); + } + } + + protected void handleAddKey(PubkeyBean keybean, String password) { + KeyPair pair = null; + if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(keybean.getType())) { + // load specific key using pem format + try { + pair = PEMDecoder.decode(new String(keybean.getPrivateKey()).toCharArray(), password); + } catch(Exception e) { + String message = getResources().getString(R.string.pubkey_failed_add, keybean.getNickname()); + Log.e(TAG, message, e); + Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG).show(); + } + } else { + // load using internal generated format + try { + PrivateKey privKey = PubkeyUtils.decodePrivate(keybean.getPrivateKey(), keybean.getType(), password); + PublicKey pubKey = PubkeyUtils.decodePublic(keybean.getPublicKey(), keybean.getType()); + Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey)); + + pair = new KeyPair(pubKey, privKey); + } catch (Exception e) { + String message = getResources().getString(R.string.pubkey_failed_add, keybean.getNickname()); + Log.e(TAG, message, e); + Toast.makeText(PubkeyListActivity.this, message, Toast.LENGTH_LONG).show(); + return; + } + } + + if (pair == null) { + return; + } + + Log.d(TAG, String.format("Unlocked key '%s'", keybean.getNickname())); + + // save this key in memory + bound.addKey(keybean, pair, true); + + updateList(); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + // Create menu to handle deleting and editing pubkey + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + final PubkeyBean pubkey = (PubkeyBean) getListView().getItemAtPosition(info.position); + + menu.setHeaderTitle(pubkey.getNickname()); + + // TODO: option load/unload key from in-memory list + // prompt for password as needed for passworded keys + + // cant change password or clipboard imported keys + final boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); + final boolean loaded = bound.isKeyLoaded(pubkey.getNickname()); + + MenuItem load = menu.add(loaded ? R.string.pubkey_memory_unload : R.string.pubkey_memory_load); + load.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + if(loaded) { + bound.removeKey(pubkey.getNickname()); + updateList(); + } else { + handleAddKey(pubkey); + //bound.addKey(nickname, trileadKey); + } + return true; + } + }); + + onstartToggle = menu.add(R.string.pubkey_load_on_start); + onstartToggle.setEnabled(!pubkey.isEncrypted()); + onstartToggle.setCheckable(true); + onstartToggle.setChecked(pubkey.isStartup()); + onstartToggle.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // toggle onstart status + pubkey.setStartup(!pubkey.isStartup()); + pubkeydb.savePubkey(pubkey); + updateList(); + return true; + } + }); + + MenuItem copyPublicToClipboard = menu.add(R.string.pubkey_copy_public); + copyPublicToClipboard.setEnabled(!imported); + copyPublicToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + PublicKey pk = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); + String openSSHPubkey = PubkeyUtils.convertToOpenSSHFormat(pk, pubkey.getNickname()); + + clipboard.setText(openSSHPubkey); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + }); + + MenuItem copyPrivateToClipboard = menu.add(R.string.pubkey_copy_private); + copyPrivateToClipboard.setEnabled(!pubkey.isEncrypted() || imported); + copyPrivateToClipboard.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + try { + String data = null; + + if (imported) + data = new String(pubkey.getPrivateKey()); + else { + PrivateKey pk = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType()); + data = PubkeyUtils.exportPEM(pk, null); + } + + clipboard.setText(data); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + }); + + MenuItem changePassword = menu.add(R.string.pubkey_change_password); + changePassword.setEnabled(!imported); + changePassword.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + final View changePasswordView = inflater.inflate(R.layout.dia_changepassword, null, false); + ((TableRow)changePasswordView.findViewById(R.id.old_password_prompt)) + .setVisibility(pubkey.isEncrypted() ? View.VISIBLE : View.GONE); + new AlertDialog.Builder(PubkeyListActivity.this) + .setView(changePasswordView) + .setPositiveButton(R.string.button_change, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String oldPassword = ((EditText)changePasswordView.findViewById(R.id.old_password)).getText().toString(); + String password1 = ((EditText)changePasswordView.findViewById(R.id.password1)).getText().toString(); + String password2 = ((EditText)changePasswordView.findViewById(R.id.password2)).getText().toString(); + + if (!password1.equals(password2)) { + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_passwords_do_not_match_msg) + .setPositiveButton(android.R.string.ok, null) + .create().show(); + return; + } + + try { + if (!pubkey.changePassword(oldPassword, password1)) + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_wrong_password_msg) + .setPositiveButton(android.R.string.ok, null) + .create().show(); + else { + pubkeydb.savePubkey(pubkey); + updateList(); + } + } catch (Exception e) { + Log.e(TAG, "Could not change private key password", e); + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_key_corrupted_msg) + .setPositiveButton(android.R.string.ok, null) + .create().show(); + } + } + }) + .setNegativeButton(android.R.string.cancel, null).create().show(); + + return true; + } + }); + + confirmUse = menu.add(R.string.pubkey_confirm_use); + confirmUse.setCheckable(true); + confirmUse.setChecked(pubkey.isConfirmUse()); + confirmUse.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // toggle confirm use + pubkey.setConfirmUse(!pubkey.isConfirmUse()); + pubkeydb.savePubkey(pubkey); + updateList(); + return true; + } + }); + + MenuItem delete = menu.add(R.string.pubkey_delete); + delete.setOnMenuItemClickListener(new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // prompt user to make sure they really want this + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(getString(R.string.delete_message, pubkey.getNickname())) + .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + + // dont forget to remove from in-memory + if(loaded) + bound.removeKey(pubkey.getNickname()); + + // delete from backend database and update gui + pubkeydb.deletePubkey(pubkey); + updateList(); + } + }) + .setNegativeButton(R.string.delete_neg, null).create().show(); + + return true; + } + }); + + } + + protected void updateList() { + if (pubkeydb == null) return; + + pubkeys = pubkeydb.allPubkeys(); + PubkeyAdapter adapter = new PubkeyAdapter(this, pubkeys); + + this.setListAdapter(adapter); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + + switch (requestCode) { + case REQUEST_CODE_PICK_FILE: + if (resultCode == RESULT_OK && intent != null) { + Uri uri = intent.getData(); + try { + if (uri != null) { + readKeyFromFile(new File(URI.create(uri.toString()))); + } else { + String filename = intent.getDataString(); + if (filename != null) + readKeyFromFile(new File(URI.create(filename))); + } + } catch (IllegalArgumentException e) { + Log.e(TAG, "Couldn't read from picked file", e); + } + } + break; + } + } + + /** + * @param name + */ + private void readKeyFromFile(File file) { + PubkeyBean pubkey = new PubkeyBean(); + + // find the exact file selected + pubkey.setNickname(file.getName()); + + if (file.length() > MAX_KEYFILE_SIZE) { + Toast.makeText(PubkeyListActivity.this, + R.string.pubkey_import_parse_problem, + Toast.LENGTH_LONG).show(); + return; + } + + // parse the actual key once to check if its encrypted + // then save original file contents into our database + try { + byte[] raw = readRaw(file); + + String data = new String(raw); + if (data.startsWith(PubkeyUtils.PKCS8_START)) { + int start = data.indexOf(PubkeyUtils.PKCS8_START) + PubkeyUtils.PKCS8_START.length(); + int end = data.indexOf(PubkeyUtils.PKCS8_END); + + if (end > start) { + char[] encoded = data.substring(start, end - 1).toCharArray(); + Log.d(TAG, "encoded: " + new String(encoded)); + byte[] decoded = Base64.decode(encoded); + + KeyPair kp = PubkeyUtils.recoverKeyPair(decoded); + + pubkey.setType(kp.getPrivate().getAlgorithm()); + pubkey.setPrivateKey(kp.getPrivate().getEncoded()); + pubkey.setPublicKey(kp.getPublic().getEncoded()); + } else { + Log.e(TAG, "Problem parsing PKCS#8 file; corrupt?"); + Toast.makeText(PubkeyListActivity.this, + R.string.pubkey_import_parse_problem, + Toast.LENGTH_LONG).show(); + } + } else { + PEMStructure struct = PEMDecoder.parsePEM(new String(raw).toCharArray()); + pubkey.setEncrypted(PEMDecoder.isPEMEncrypted(struct)); + pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED); + pubkey.setPrivateKey(raw); + } + + // write new value into database + if (pubkeydb == null) + pubkeydb = new PubkeyDatabase(this); + pubkeydb.savePubkey(pubkey); + + updateList(); + } catch(Exception e) { + Log.e(TAG, "Problem parsing imported private key", e); + Toast.makeText(PubkeyListActivity.this, R.string.pubkey_import_parse_problem, Toast.LENGTH_LONG).show(); + } + } + + /** + * + */ + private void pickFileSimple() { + // build list of all files in sdcard root + final File sdcard = Environment.getExternalStorageDirectory(); + Log.d(TAG, sdcard.toString()); + + // Don't show a dialog if the SD card is completely absent. + final String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED_READ_ONLY.equals(state) + && !Environment.MEDIA_MOUNTED.equals(state)) { + new AlertDialog.Builder(PubkeyListActivity.this) + .setMessage(R.string.alert_sdcard_absent) + .setNegativeButton(android.R.string.cancel, null).create().show(); + return; + } + + List names = new LinkedList(); + { + File[] files = sdcard.listFiles(); + if (files != null) { + for(File file : sdcard.listFiles()) { + if(file.isDirectory()) continue; + names.add(file.getName()); + } + } + } + Collections.sort(names); + + final String[] namesList = names.toArray(new String[] {}); + Log.d(TAG, names.toString()); + + // prompt user to select any file from the sdcard root + new AlertDialog.Builder(PubkeyListActivity.this) + .setTitle(R.string.pubkey_list_pick) + .setItems(namesList, new OnClickListener() { + public void onClick(DialogInterface arg0, int arg1) { + String name = namesList[arg1]; + + readKeyFromFile(new File(sdcard, name)); + } + }) + .setNegativeButton(android.R.string.cancel, null).create().show(); + } + + class PubkeyAdapter extends ArrayAdapter { + private List pubkeys; + + class ViewHolder { + public TextView nickname; + public TextView caption; + public ImageView icon; + } + + public PubkeyAdapter(Context context, List pubkeys) { + super(context, R.layout.item_pubkey, pubkeys); + + this.pubkeys = pubkeys; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_pubkey, null, false); + + holder = new ViewHolder(); + + holder.nickname = (TextView) convertView.findViewById(android.R.id.text1); + holder.caption = (TextView) convertView.findViewById(android.R.id.text2); + holder.icon = (ImageView) convertView.findViewById(android.R.id.icon1); + + convertView.setTag(holder); + } else + holder = (ViewHolder) convertView.getTag(); + + PubkeyBean pubkey = pubkeys.get(position); + holder.nickname.setText(pubkey.getNickname()); + + boolean imported = PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType()); + + if (imported) { + try { + PEMStructure struct = PEMDecoder.parsePEM(new String(pubkey.getPrivateKey()).toCharArray()); + String type = (struct.pemType == PEMDecoder.PEM_RSA_PRIVATE_KEY) ? "RSA" : "DSA"; + holder.caption.setText(String.format("%s unknown-bit", type)); + } catch (IOException e) { + Log.e(TAG, "Error decoding IMPORTED public key at " + pubkey.getId(), e); + } + } else { + try { + holder.caption.setText(pubkey.getDescription()); + } catch (Exception e) { + Log.e(TAG, "Error decoding public key at " + pubkey.getId(), e); + holder.caption.setText(R.string.pubkey_unknown_format); + } + } + + if (bound == null) { + holder.icon.setVisibility(View.GONE); + } else { + holder.icon.setVisibility(View.VISIBLE); + + if (bound.isKeyLoaded(pubkey.getNickname())) + holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true); + else + holder.icon.setImageState(new int[] { }, true); + } + + return convertView; + } + } +} diff --git a/app/src/main/java/org/connectbot/SettingsActivity.java b/app/src/main/java/org/connectbot/SettingsActivity.java new file mode 100644 index 0000000..460805d --- /dev/null +++ b/app/src/main/java/org/connectbot/SettingsActivity.java @@ -0,0 +1,60 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import org.connectbot.util.PreferenceConstants; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; +import android.util.Log; + +public class SettingsActivity extends PreferenceActivity { + private static final String TAG = "ConnectBot.Settings"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + addPreferencesFromResource(R.xml.preferences); + } catch (ClassCastException e) { + Log.e(TAG, "Shared preferences are corrupt! Resetting to default values."); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Blow away all the preferences + SharedPreferences.Editor editor = preferences.edit(); + editor.clear(); + editor.commit(); + + PreferenceManager.setDefaultValues(this, R.xml.preferences, true); + + // Since they were able to get to the Settings activity, they already agreed to the EULA + editor = preferences.edit(); + editor.putBoolean(PreferenceConstants.EULA, true); + editor.commit(); + + addPreferencesFromResource(R.xml.preferences); + } + + // TODO: add parse checking here to make sure we have integer value for scrollback + + } + +} diff --git a/app/src/main/java/org/connectbot/StrictModeSetup.java b/app/src/main/java/org/connectbot/StrictModeSetup.java new file mode 100644 index 0000000..3a2000e --- /dev/null +++ b/app/src/main/java/org/connectbot/StrictModeSetup.java @@ -0,0 +1,23 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.connectbot; +import android.os.StrictMode; +public class StrictModeSetup { + public static void run() { + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX); + } +} diff --git a/app/src/main/java/org/connectbot/TerminalView.java b/app/src/main/java/org/connectbot/TerminalView.java new file mode 100644 index 0000000..02683c2 --- /dev/null +++ b/app/src/main/java/org/connectbot/TerminalView.java @@ -0,0 +1,452 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.connectbot.bean.SelectionArea; +import org.connectbot.service.FontSizeChangedListener; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalKeyListener; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelXorXfermode; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.Toast; +import de.mud.terminal.VDUBuffer; + +/** + * User interface {@link View} for showing a TerminalBridge in an + * {@link Activity}. Handles drawing bitmap updates and passing keystrokes down + * to terminal. + * + * @author jsharkey + */ +public class TerminalView extends View implements FontSizeChangedListener { + + private final Context context; + public final TerminalBridge bridge; + private final Paint paint; + private final Paint cursorPaint; + private final Paint cursorStrokePaint; + + // Cursor paints to distinguish modes + private Path ctrlCursor, altCursor, shiftCursor; + private RectF tempSrc, tempDst; + private Matrix scaleMatrix; + private static final Matrix.ScaleToFit scaleType = Matrix.ScaleToFit.FILL; + + private Toast notification = null; + private String lastNotification = null; + private volatile boolean notifications = true; + + // Related to Accessibility Features + private boolean mAccessibilityInitialized = false; + private boolean mAccessibilityActive = true; + private Object[] mAccessibilityLock = new Object[0]; + private StringBuffer mAccessibilityBuffer; + private Pattern mControlCodes = null; + private Matcher mCodeMatcher = null; + private AccessibilityEventSender mEventSender = null; + + private static final String BACKSPACE_CODE = "\\x08\\x1b\\[K"; + private static final String CONTROL_CODE_PATTERN = "\\x1b\\[K[^m]+[m|:]"; + + private static final int ACCESSIBILITY_EVENT_THRESHOLD = 1000; + private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService"; + private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN"; + + public TerminalView(Context context, TerminalBridge bridge) { + super(context); + + this.context = context; + this.bridge = bridge; + paint = new Paint(); + + setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); + setFocusable(true); + setFocusableInTouchMode(true); + + cursorPaint = new Paint(); + cursorPaint.setColor(bridge.color[bridge.defaultFg]); + cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg])); + cursorPaint.setAntiAlias(true); + + cursorStrokePaint = new Paint(cursorPaint); + cursorStrokePaint.setStrokeWidth(0.1f); + cursorStrokePaint.setStyle(Paint.Style.STROKE); + + /* + * Set up our cursor indicators on a 1x1 Path object which we can later + * transform to our character width and height + */ + // TODO make this into a resource somehow + shiftCursor = new Path(); + shiftCursor.lineTo(0.5f, 0.33f); + shiftCursor.lineTo(1.0f, 0.0f); + + altCursor = new Path(); + altCursor.moveTo(0.0f, 1.0f); + altCursor.lineTo(0.5f, 0.66f); + altCursor.lineTo(1.0f, 1.0f); + + ctrlCursor = new Path(); + ctrlCursor.moveTo(0.0f, 0.25f); + ctrlCursor.lineTo(1.0f, 0.5f); + ctrlCursor.lineTo(0.0f, 0.75f); + + // For creating the transform when the terminal resizes + tempSrc = new RectF(); + tempSrc.set(0.0f, 0.0f, 1.0f, 1.0f); + tempDst = new RectF(); + scaleMatrix = new Matrix(); + + bridge.addFontSizeChangedListener(this); + + // connect our view up to the bridge + setOnKeyListener(bridge.getKeyHandler()); + + mAccessibilityBuffer = new StringBuffer(); + + // Enable accessibility features if a screen reader is active. + new AccessibilityStateTester().execute((Void) null); + } + + public void destroy() { + // tell bridge to destroy its bitmap + bridge.parentDestroyed(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + bridge.parentChanged(this); + + scaleCursors(); + } + + public void onFontSizeChanged(float size) { + scaleCursors(); + } + + private void scaleCursors() { + // Create a scale matrix to scale our 1x1 representation of the cursor + tempDst.set(0.0f, 0.0f, bridge.charWidth, bridge.charHeight); + scaleMatrix.setRectToRect(tempSrc, tempDst, scaleType); + } + + @Override + public void onDraw(Canvas canvas) { + if(bridge.bitmap != null) { + // draw the bitmap + bridge.onDraw(); + + // draw the bridge bitmap if it exists + canvas.drawBitmap(bridge.bitmap, 0, 0, paint); + + // also draw cursor if visible + if (bridge.buffer.isCursorVisible()) { + int cursorColumn = bridge.buffer.getCursorColumn(); + final int cursorRow = bridge.buffer.getCursorRow(); + + final int columns = bridge.buffer.getColumns(); + + if (cursorColumn == columns) + cursorColumn = columns - 1; + + if (cursorColumn < 0 || cursorRow < 0) + return; + + int currentAttribute = bridge.buffer.getAttributes( + cursorColumn, cursorRow); + boolean onWideCharacter = (currentAttribute & VDUBuffer.FULLWIDTH) != 0; + + int x = cursorColumn * bridge.charWidth; + int y = (bridge.buffer.getCursorRow() + + bridge.buffer.screenBase - bridge.buffer.windowBase) + * bridge.charHeight; + + // Save the current clip and translation + canvas.save(); + + canvas.translate(x, y); + canvas.clipRect(0, 0, + bridge.charWidth * (onWideCharacter ? 2 : 1), + bridge.charHeight); + canvas.drawPaint(cursorPaint); + + final int deadKey = bridge.getKeyHandler().getDeadKey(); + if (deadKey != 0) { + canvas.drawText(new char[] { (char)deadKey }, 0, 1, 0, 0, cursorStrokePaint); + } + + // Make sure we scale our decorations to the correct size. + canvas.concat(scaleMatrix); + + int metaState = bridge.getKeyHandler().getMetaState(); + + if ((metaState & TerminalKeyListener.OUR_SHIFT_ON) != 0) + canvas.drawPath(shiftCursor, cursorStrokePaint); + else if ((metaState & TerminalKeyListener.OUR_SHIFT_LOCK) != 0) + canvas.drawPath(shiftCursor, cursorPaint); + + if ((metaState & TerminalKeyListener.OUR_ALT_ON) != 0) + canvas.drawPath(altCursor, cursorStrokePaint); + else if ((metaState & TerminalKeyListener.OUR_ALT_LOCK) != 0) + canvas.drawPath(altCursor, cursorPaint); + + if ((metaState & TerminalKeyListener.OUR_CTRL_ON) != 0) + canvas.drawPath(ctrlCursor, cursorStrokePaint); + else if ((metaState & TerminalKeyListener.OUR_CTRL_LOCK) != 0) + canvas.drawPath(ctrlCursor, cursorPaint); + + // Restore previous clip region + canvas.restore(); + } + + // draw any highlighted area + if (bridge.isSelectingForCopy()) { + SelectionArea area = bridge.getSelectionArea(); + canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRect( + area.getLeft() * bridge.charWidth, + area.getTop() * bridge.charHeight, + (area.getRight() + 1) * bridge.charWidth, + (area.getBottom() + 1) * bridge.charHeight + ); + canvas.drawPaint(cursorPaint); + canvas.restore(); + } + } + } + + public void notifyUser(String message) { + if (!notifications) + return; + + if (notification != null) { + // Don't keep telling the user the same thing. + if (lastNotification != null && lastNotification.equals(message)) + return; + + notification.setText(message); + notification.show(); + } else { + notification = Toast.makeText(context, message, Toast.LENGTH_SHORT); + notification.show(); + } + + lastNotification = message; + } + + /** + * Ask the {@link TerminalBridge} we're connected to to resize to a specific size. + * @param width + * @param height + */ + public void forceSize(int width, int height) { + bridge.resizeComputed(width, height, getWidth(), getHeight()); + } + + /** + * Sets the ability for the TerminalView to display Toast notifications to the user. + * @param value whether to enable notifications or not + */ + public void setNotifications(boolean value) { + notifications = value; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + outAttrs.imeOptions |= + EditorInfo.IME_FLAG_NO_EXTRACT_UI | + EditorInfo.IME_FLAG_NO_ENTER_ACTION | + EditorInfo.IME_ACTION_NONE; + outAttrs.inputType = EditorInfo.TYPE_NULL; + return new BaseInputConnection(this, false) { + @Override + public boolean deleteSurroundingText (int leftLength, int rightLength) { + if (rightLength == 0 && leftLength == 0) { + return this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); + } + for (int i = 0; i < leftLength; i++) { + this.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); + } + // TODO: forward delete + return true; + } + }; + } + + public void propagateConsoleText(char[] rawText, int length) { + if (mAccessibilityActive) { + synchronized (mAccessibilityLock) { + mAccessibilityBuffer.append(rawText, 0, length); + } + + if (mAccessibilityInitialized) { + if (mEventSender != null) { + removeCallbacks(mEventSender); + } else { + mEventSender = new AccessibilityEventSender(); + } + + postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD); + } + } + } + + private class AccessibilityEventSender implements Runnable { + public void run() { + synchronized (mAccessibilityLock) { + if (mCodeMatcher == null) { + mCodeMatcher = mControlCodes.matcher(mAccessibilityBuffer); + } else { + mCodeMatcher.reset(mAccessibilityBuffer); + } + + // Strip all control codes out. + mAccessibilityBuffer = new StringBuffer(mCodeMatcher.replaceAll(" ")); + + // Apply Backspaces using backspace character sequence + int i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE); + while (i != -1) { + mAccessibilityBuffer = mAccessibilityBuffer.replace(i == 0 ? 0 : i - 1, i + + BACKSPACE_CODE.length(), ""); + i = mAccessibilityBuffer.indexOf(BACKSPACE_CODE); + } + + if (mAccessibilityBuffer.length() > 0) { + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + event.setFromIndex(0); + event.setAddedCount(mAccessibilityBuffer.length()); + event.getText().add(mAccessibilityBuffer); + + sendAccessibilityEventUnchecked(event); + mAccessibilityBuffer.setLength(0); + } + } + } + } + + private class AccessibilityStateTester extends AsyncTask { + @Override + protected Boolean doInBackground(Void... params) { + /* + * Presumably if the accessibility manager is not enabled, we don't + * need to send accessibility events. + */ + final AccessibilityManager accessibility = (AccessibilityManager) context + .getSystemService(Context.ACCESSIBILITY_SERVICE); + if (!accessibility.isEnabled()) { + return false; + } + + /* + * Restrict the set of intents to only accessibility services that + * have the category FEEDBACK_SPOKEN (aka, screen readers). + */ + final Intent screenReaderIntent = new Intent(SCREENREADER_INTENT_ACTION); + screenReaderIntent.addCategory(SCREENREADER_INTENT_CATEGORY); + + final ContentResolver cr = context.getContentResolver(); + + final List screenReaders = context.getPackageManager().queryIntentServices( + screenReaderIntent, 0); + + boolean foundScreenReader = false; + + final int N = screenReaders.size(); + for (int i = 0; i < N; i++) { + final ResolveInfo screenReader = screenReaders.get(i); + + /* + * All screen readers are expected to implement a content + * provider that responds to: + * content://.providers.StatusProvider + */ + final Cursor cursor = cr.query( + Uri.parse("content://" + screenReader.serviceInfo.packageName + + ".providers.StatusProvider"), null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + /* + * These content providers use a special cursor that only has + * one element, an integer that is 1 if the screen reader is + * running. + */ + final int status = cursor.getInt(0); + + cursor.close(); + + if (status == 1) { + foundScreenReader = true; + break; + } + } + } + + if (foundScreenReader) { + mControlCodes = Pattern.compile(CONTROL_CODE_PATTERN); + } + + return foundScreenReader; + } + + @Override + protected void onPostExecute(Boolean result) { + mAccessibilityActive = result; + + mAccessibilityInitialized = true; + + if (result) { + mEventSender = new AccessibilityEventSender(); + postDelayed(mEventSender, ACCESSIBILITY_EVENT_THRESHOLD); + } else { + mAccessibilityBuffer = null; + } + } + } +} diff --git a/app/src/main/java/org/connectbot/WizardActivity.java b/app/src/main/java/org/connectbot/WizardActivity.java new file mode 100644 index 0000000..35a60ca --- /dev/null +++ b/app/src/main/java/org/connectbot/WizardActivity.java @@ -0,0 +1,105 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot; + +import org.connectbot.util.HelpTopicView; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.ViewFlipper; + +/** + * Show a series of wizard-like steps to the user, which might include an EULA, + * program credits, and helpful hints. + * + * @author jsharkey + */ +public class WizardActivity extends Activity { + protected ViewFlipper flipper = null; + private Button next, prev; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.act_wizard); + + this.flipper = (ViewFlipper) findViewById(R.id.wizard_flipper); + + // inflate the layout for EULA step + LayoutInflater inflater = LayoutInflater.from(this); + this.flipper.addView(inflater.inflate(R.layout.wiz_eula, this.flipper, false)); + + // Add a view for each help topic we want the user to see. + String[] topics = getResources().getStringArray(R.array.list_wizard_topics); + for (String topic : topics) { + flipper.addView(new HelpTopicView(this).setTopic(topic)); + } + + next = (Button)this.findViewById(R.id.action_next); + next.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if(isLastDisplayed()) { + // user walked past end of wizard, so return okay + WizardActivity.this.setResult(Activity.RESULT_OK); + WizardActivity.this.finish(); + } else { + // show next step and update buttons + flipper.showNext(); + updateButtons(); + } + } + }); + + prev = (Button)this.findViewById(R.id.action_prev); + prev.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if(isFirstDisplayed()) { + // user walked past beginning of wizard, so return that they cancelled + WizardActivity.this.setResult(Activity.RESULT_CANCELED); + WizardActivity.this.finish(); + } else { + // show previous step and update buttons + flipper.showPrevious(); + updateButtons(); + } + } + }); + + this.updateButtons(); + } + + protected boolean isFirstDisplayed() { + return (flipper.getDisplayedChild() == 0); + } + + protected boolean isLastDisplayed() { + return (flipper.getDisplayedChild() == flipper.getChildCount() - 1); + } + + protected void updateButtons() { + boolean eula = (flipper.getDisplayedChild() == 0); + + next.setText(eula ? getString(R.string.wizard_agree) : getString(R.string.wizard_next)); + prev.setText(eula ? getString(R.string.delete_neg) : getString(R.string.wizard_back)); + } +} diff --git a/app/src/main/java/org/connectbot/bean/AbstractBean.java b/app/src/main/java/org/connectbot/bean/AbstractBean.java new file mode 100644 index 0000000..7f55785 --- /dev/null +++ b/app/src/main/java/org/connectbot/bean/AbstractBean.java @@ -0,0 +1,49 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.bean; + +import java.util.Map.Entry; + +import org.connectbot.util.XmlBuilder; + +import android.content.ContentValues; + +/** + * @author Kenny Root + * + */ +abstract class AbstractBean { + public abstract ContentValues getValues(); + public abstract String getBeanName(); + + public String toXML() { + XmlBuilder xml = new XmlBuilder(); + + xml.append(String.format("<%s>", getBeanName())); + + ContentValues values = getValues(); + for (Entry entry : values.valueSet()) { + Object value = entry.getValue(); + if (value != null) + xml.append(entry.getKey(), value); + } + xml.append(String.format("", getBeanName())); + + return xml.toString(); + } +} diff --git a/app/src/main/java/org/connectbot/bean/HostBean.java b/app/src/main/java/org/connectbot/bean/HostBean.java new file mode 100644 index 0000000..2fd7bfb --- /dev/null +++ b/app/src/main/java/org/connectbot/bean/HostBean.java @@ -0,0 +1,317 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.bean; + +import org.connectbot.util.HostDatabase; + +import android.content.ContentValues; +import android.net.Uri; + +/** + * @author Kenny Root + * + */ +public class HostBean extends AbstractBean { + public static final String BEAN_NAME = "host"; + + /* Database fields */ + private long id = -1; + private String nickname = null; + private String username = null; + private String hostname = null; + private int port = 22; + private String protocol = "ssh"; + private String hostKeyAlgo = null; + private byte[] hostKey = null; + private long lastConnect = -1; + private String color; + private boolean useKeys = true; + private String useAuthAgent = HostDatabase.AUTHAGENT_NO; + private String postLogin = null; + private long pubkeyId = -1; + private boolean wantSession = true; + private String delKey = HostDatabase.DELKEY_DEL; + private int fontSize = -1; + private boolean compression = false; + private String encoding = HostDatabase.ENCODING_DEFAULT; + private boolean stayConnected = false; + + public HostBean() { + + } + + @Override + public String getBeanName() { + return BEAN_NAME; + } + + public HostBean(String nickname, String protocol, String username, String hostname, int port) { + this.nickname = nickname; + this.protocol = protocol; + this.username = username; + this.hostname = hostname; + this.port = port; + } + + public void setId(long id) { + this.id = id; + } + public long getId() { + return id; + } + public void setNickname(String nickname) { + this.nickname = nickname; + } + public String getNickname() { + return nickname; + } + public void setUsername(String username) { + this.username = username; + } + public String getUsername() { + return username; + } + public void setHostname(String hostname) { + this.hostname = hostname; + } + public String getHostname() { + return hostname; + } + public void setPort(int port) { + this.port = port; + } + public int getPort() { + return port; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return protocol; + } + + public void setHostKeyAlgo(String hostKeyAlgo) { + this.hostKeyAlgo = hostKeyAlgo; + } + public String getHostKeyAlgo() { + return hostKeyAlgo; + } + public void setHostKey(byte[] hostKey) { + if (hostKey == null) + this.hostKey = null; + else + this.hostKey = hostKey.clone(); + } + public byte[] getHostKey() { + if (hostKey == null) + return null; + else + return hostKey.clone(); + } + public void setLastConnect(long lastConnect) { + this.lastConnect = lastConnect; + } + public long getLastConnect() { + return lastConnect; + } + public void setColor(String color) { + this.color = color; + } + public String getColor() { + return color; + } + public void setUseKeys(boolean useKeys) { + this.useKeys = useKeys; + } + public boolean getUseKeys() { + return useKeys; + } + public void setUseAuthAgent(String useAuthAgent) { + this.useAuthAgent = useAuthAgent; + } + public String getUseAuthAgent() { + return useAuthAgent; + } + public void setPostLogin(String postLogin) { + this.postLogin = postLogin; + } + public String getPostLogin() { + return postLogin; + } + public void setPubkeyId(long pubkeyId) { + this.pubkeyId = pubkeyId; + } + public long getPubkeyId() { + return pubkeyId; + } + public void setWantSession(boolean wantSession) { + this.wantSession = wantSession; + } + public boolean getWantSession() { + return wantSession; + } + public void setDelKey(String delKey) { + this.delKey = delKey; + } + public String getDelKey() { + return delKey; + } + public void setFontSize(int fontSize) { + this.fontSize = fontSize; + } + public int getFontSize() { + return fontSize; + } + public void setCompression(boolean compression) { + this.compression = compression; + } + public boolean getCompression() { + return compression; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public String getEncoding() { + return this.encoding; + } + + public void setStayConnected(boolean stayConnected) { + this.stayConnected = stayConnected; + } + + public boolean getStayConnected() { + return stayConnected; + } + + public String getDescription() { + String description = String.format("%s@%s", username, hostname); + + if (port != 22) + description += String.format(":%d", port); + + return description; + } + + @Override + public ContentValues getValues() { + ContentValues values = new ContentValues(); + + values.put(HostDatabase.FIELD_HOST_NICKNAME, nickname); + values.put(HostDatabase.FIELD_HOST_PROTOCOL, protocol); + values.put(HostDatabase.FIELD_HOST_USERNAME, username); + values.put(HostDatabase.FIELD_HOST_HOSTNAME, hostname); + values.put(HostDatabase.FIELD_HOST_PORT, port); + values.put(HostDatabase.FIELD_HOST_HOSTKEYALGO, hostKeyAlgo); + values.put(HostDatabase.FIELD_HOST_HOSTKEY, hostKey); + values.put(HostDatabase.FIELD_HOST_LASTCONNECT, lastConnect); + values.put(HostDatabase.FIELD_HOST_COLOR, color); + values.put(HostDatabase.FIELD_HOST_USEKEYS, Boolean.toString(useKeys)); + values.put(HostDatabase.FIELD_HOST_USEAUTHAGENT, useAuthAgent); + values.put(HostDatabase.FIELD_HOST_POSTLOGIN, postLogin); + values.put(HostDatabase.FIELD_HOST_PUBKEYID, pubkeyId); + values.put(HostDatabase.FIELD_HOST_WANTSESSION, Boolean.toString(wantSession)); + values.put(HostDatabase.FIELD_HOST_DELKEY, delKey); + values.put(HostDatabase.FIELD_HOST_FONTSIZE, fontSize); + values.put(HostDatabase.FIELD_HOST_COMPRESSION, Boolean.toString(compression)); + values.put(HostDatabase.FIELD_HOST_ENCODING, encoding); + values.put(HostDatabase.FIELD_HOST_STAYCONNECTED, stayConnected); + + return values; + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof HostBean)) + return false; + + HostBean host = (HostBean)o; + + if (id != -1 && host.getId() != -1) + return host.getId() == id; + + if (nickname == null) { + if (host.getNickname() != null) + return false; + } else if (!nickname.equals(host.getNickname())) + return false; + + if (protocol == null) { + if (host.getProtocol() != null) + return false; + } else if (!protocol.equals(host.getProtocol())) + return false; + + if (username == null) { + if (host.getUsername() != null) + return false; + } else if (!username.equals(host.getUsername())) + return false; + + if (hostname == null) { + if (host.getHostname() != null) + return false; + } else if (!hostname.equals(host.getHostname())) + return false; + + if (port != host.getPort()) + return false; + + return true; + } + + @Override + public int hashCode() { + int hash = 7; + + if (id != -1) + return (int)id; + + hash = 31 * hash + (null == nickname ? 0 : nickname.hashCode()); + hash = 31 * hash + (null == protocol ? 0 : protocol.hashCode()); + hash = 31 * hash + (null == username ? 0 : username.hashCode()); + hash = 31 * hash + (null == hostname ? 0 : hostname.hashCode()); + hash = 31 * hash + port; + + return hash; + } + + /** + * @return URI identifying this HostBean + */ + public Uri getUri() { + StringBuilder sb = new StringBuilder(); + sb.append(protocol) + .append("://"); + + if (username != null) + sb.append(Uri.encode(username)) + .append('@'); + + sb.append(Uri.encode(hostname)) + .append(':') + .append(port) + .append("/#") + .append(nickname); + return Uri.parse(sb.toString()); + } + +} diff --git a/app/src/main/java/org/connectbot/bean/PortForwardBean.java b/app/src/main/java/org/connectbot/bean/PortForwardBean.java new file mode 100644 index 0000000..2bdaf20 --- /dev/null +++ b/app/src/main/java/org/connectbot/bean/PortForwardBean.java @@ -0,0 +1,239 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.bean; + +import org.connectbot.util.HostDatabase; + +import android.content.ContentValues; + + +/** + * @author Kenny Root + * + */ +public class PortForwardBean extends AbstractBean { + public static final String BEAN_NAME = "portforward"; + + /* Database fields */ + private long id = -1; + private long hostId = -1; + private String nickname = null; + private String type = null; + private int sourcePort = -1; + private String destAddr = null; + private int destPort = -1; + + /* Transient values */ + private boolean enabled = false; + private Object identifier = null; + + /** + * @param id database ID of port forward + * @param nickname Nickname to use to identify port forward + * @param type One of the port forward types from {@link HostDatabase} + * @param sourcePort Source port number + * @param destAddr Destination hostname or IP address + * @param destPort Destination port number + */ + public PortForwardBean(long id, long hostId, String nickname, String type, int sourcePort, String destAddr, int destPort) { + this.id = id; + this.hostId = hostId; + this.nickname = nickname; + this.type = type; + this.sourcePort = sourcePort; + this.destAddr = destAddr; + this.destPort = destPort; + } + + /** + * @param type One of the port forward types from {@link HostDatabase} + * @param source Source port number + * @param dest Destination is "host:port" format + */ + public PortForwardBean(long hostId, String nickname, String type, String source, String dest) { + this.hostId = hostId; + this.nickname = nickname; + this.type = type; + this.sourcePort = Integer.parseInt(source); + + setDest(dest); + } + + public String getBeanName() { + return BEAN_NAME; + } + + /** + * @param id the id to set + */ + public void setId(long id) { + this.id = id; + } + + /** + * @return the id + */ + public long getId() { + return id; + } + + /** + * @param nickname the nickname to set + */ + public void setNickname(String nickname) { + this.nickname = nickname; + } + + /** + * @return the nickname + */ + public String getNickname() { + return nickname; + } + + /** + * @param type the type to set + */ + public void setType(String type) { + this.type = type; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param sourcePort the sourcePort to set + */ + public void setSourcePort(int sourcePort) { + this.sourcePort = sourcePort; + } + + /** + * @return the sourcePort + */ + public int getSourcePort() { + return sourcePort; + } + + /** + * @param dest The destination in "host:port" format + */ + public final void setDest(String dest) { + String[] destSplit = dest.split(":"); + this.destAddr = destSplit[0]; + if (destSplit.length > 1) + this.destPort = Integer.parseInt(destSplit[1]); + } + + /** + * @param destAddr the destAddr to set + */ + public void setDestAddr(String destAddr) { + this.destAddr = destAddr; + } + + /** + * @return the destAddr + */ + public String getDestAddr() { + return destAddr; + } + + /** + * @param destPort the destPort to set + */ + public void setDestPort(int destPort) { + this.destPort = destPort; + } + + /** + * @return the destPort + */ + public int getDestPort() { + return destPort; + } + + /** + * @param enabled the enabled to set + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * @return the enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * @param identifier the identifier of this particular type to set + */ + public void setIdentifier(Object identifier) { + this.identifier = identifier; + } + + /** + * @return the identifier used by this particular type + */ + public Object getIdentifier() { + return identifier; + } + + /** + * @return human readable description of the port forward + */ + public CharSequence getDescription() { + String description = "Unknown type"; + + if (HostDatabase.PORTFORWARD_LOCAL.equals(type)) { + description = String.format("Local port %d to %s:%d", sourcePort, destAddr, destPort); + } else if (HostDatabase.PORTFORWARD_REMOTE.equals(type)) { + description = String.format("Remote port %d to %s:%d", sourcePort, destAddr, destPort); +/* I don't think we need the SOCKS4 type. + } else if (HostDatabase.PORTFORWARD_DYNAMIC4.equals(type)) { + description = String.format("Dynamic port %d (SOCKS4)", sourcePort); +*/ + } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(type)) { + description = String.format("Dynamic port %d (SOCKS)", sourcePort); + } + + return description; + } + + /** + * @return + */ + public ContentValues getValues() { + ContentValues values = new ContentValues(); + + values.put(HostDatabase.FIELD_PORTFORWARD_HOSTID, hostId); + values.put(HostDatabase.FIELD_PORTFORWARD_NICKNAME, nickname); + values.put(HostDatabase.FIELD_PORTFORWARD_TYPE, type); + values.put(HostDatabase.FIELD_PORTFORWARD_SOURCEPORT, sourcePort); + values.put(HostDatabase.FIELD_PORTFORWARD_DESTADDR, destAddr); + values.put(HostDatabase.FIELD_PORTFORWARD_DESTPORT, destPort); + + return values; + } +} diff --git a/app/src/main/java/org/connectbot/bean/PubkeyBean.java b/app/src/main/java/org/connectbot/bean/PubkeyBean.java new file mode 100644 index 0000000..656c6af --- /dev/null +++ b/app/src/main/java/org/connectbot/bean/PubkeyBean.java @@ -0,0 +1,234 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.bean; + +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; + +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; + +import android.content.ContentValues; + +/** + * @author Kenny Root + * + */ +public class PubkeyBean extends AbstractBean { + public static final String BEAN_NAME = "pubkey"; + + private static final String KEY_TYPE_RSA = "RSA"; + + private static final String KEY_TYPE_DSA = "DSA"; + + private static final String KEY_TYPE_EC = "EC"; + + /* Database fields */ + private long id; + private String nickname; + private String type; + private byte[] privateKey; + private byte[] publicKey; + private boolean encrypted = false; + private boolean startup = false; + private boolean confirmUse = false; + private int lifetime = 0; + + /* Transient values */ + private transient boolean unlocked = false; + private transient Object unlockedPrivate = null; + private transient String description; + + @Override + public String getBeanName() { + return BEAN_NAME; + } + + public void setId(long id) { + this.id = id; + } + + public long getId() { + return id; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getNickname() { + return nickname; + } + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setPrivateKey(byte[] privateKey) { + if (privateKey == null) + this.privateKey = null; + else + this.privateKey = privateKey.clone(); + } + + public byte[] getPrivateKey() { + if (privateKey == null) + return null; + else + return privateKey.clone(); + } + + public void setPublicKey(byte[] encoded) { + if (encoded == null) + publicKey = null; + else + publicKey = encoded.clone(); + } + + public byte[] getPublicKey() { + if (publicKey == null) + return null; + else + return publicKey.clone(); + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } + + public boolean isEncrypted() { + return encrypted; + } + + public void setStartup(boolean startup) { + this.startup = startup; + } + + public boolean isStartup() { + return startup; + } + + public void setConfirmUse(boolean confirmUse) { + this.confirmUse = confirmUse; + } + + public boolean isConfirmUse() { + return confirmUse; + } + + public void setLifetime(int lifetime) { + this.lifetime = lifetime; + } + + public int getLifetime() { + return lifetime; + } + + public void setUnlocked(boolean unlocked) { + this.unlocked = unlocked; + } + + public boolean isUnlocked() { + return unlocked; + } + + public void setUnlockedPrivate(Object unlockedPrivate) { + this.unlockedPrivate = unlockedPrivate; + } + + public Object getUnlockedPrivate() { + return unlockedPrivate; + } + + public String getDescription() { + if (description == null) { + final StringBuilder sb = new StringBuilder(); + try { + final PublicKey pubKey = PubkeyUtils.decodePublic(privateKey, type); + if (PubkeyDatabase.KEY_TYPE_RSA.equals(type)) { + int bits = ((RSAPublicKey) pubKey).getModulus().bitLength(); + sb.append("RSA "); + sb.append(bits); + sb.append("-bit"); + } else if (PubkeyDatabase.KEY_TYPE_DSA.equals(type)) { + sb.append("DSA 1024-bit"); + } else if (PubkeyDatabase.KEY_TYPE_EC.equals(type)) { + int bits = ((ECPublicKey) pubKey).getParams().getCurve().getField() + .getFieldSize(); + sb.append("EC "); + sb.append(bits); + sb.append("-bit"); + } else { + sb.append("Unknown Key Type"); + } + } catch (NoSuchAlgorithmException e) { + sb.append("Unknown Key Type"); + } catch (InvalidKeySpecException e) { + sb.append("Unknown Key Type"); + } + + if (encrypted) + sb.append(" (encrypted)"); + + description = sb.toString(); + } + return description; + } + + /* (non-Javadoc) + * @see org.connectbot.bean.AbstractBean#getValues() + */ + @Override + public ContentValues getValues() { + ContentValues values = new ContentValues(); + + values.put(PubkeyDatabase.FIELD_PUBKEY_NICKNAME, nickname); + values.put(PubkeyDatabase.FIELD_PUBKEY_TYPE, type); + values.put(PubkeyDatabase.FIELD_PUBKEY_PRIVATE, privateKey); + values.put(PubkeyDatabase.FIELD_PUBKEY_PUBLIC, publicKey); + values.put(PubkeyDatabase.FIELD_PUBKEY_ENCRYPTED, encrypted ? 1 : 0); + values.put(PubkeyDatabase.FIELD_PUBKEY_STARTUP, startup ? 1 : 0); + values.put(PubkeyDatabase.FIELD_PUBKEY_CONFIRMUSE, confirmUse ? 1 : 0); + values.put(PubkeyDatabase.FIELD_PUBKEY_LIFETIME, lifetime); + + return values; + } + + public boolean changePassword(String oldPassword, String newPassword) throws Exception { + PrivateKey priv; + + try { + priv = PubkeyUtils.decodePrivate(getPrivateKey(), getType(), oldPassword); + } catch (Exception e) { + return false; + } + + setPrivateKey(PubkeyUtils.getEncodedPrivate(priv, newPassword)); + setEncrypted(newPassword.length() > 0); + + return true; + } +} diff --git a/app/src/main/java/org/connectbot/bean/SelectionArea.java b/app/src/main/java/org/connectbot/bean/SelectionArea.java new file mode 100644 index 0000000..4e6207d --- /dev/null +++ b/app/src/main/java/org/connectbot/bean/SelectionArea.java @@ -0,0 +1,201 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.bean; + +import de.mud.terminal.VDUBuffer; + +/** + * @author Kenny Root + * Keep track of a selection area for the terminal copying mechanism. + * If the orientation is flipped one way, swap the bottom and top or + * left and right to keep it in the correct orientation. + */ +public class SelectionArea { + private int top; + private int bottom; + private int left; + private int right; + private int maxColumns; + private int maxRows; + private boolean selectingOrigin; + + public SelectionArea() { + reset(); + } + + public final void reset() { + top = left = bottom = right = 0; + selectingOrigin = true; + } + + /** + * @param columns + * @param rows + */ + public void setBounds(int columns, int rows) { + maxColumns = columns - 1; + maxRows = rows - 1; + } + + private int checkBounds(int value, int max) { + if (value < 0) + return 0; + else if (value > max) + return max; + else + return value; + } + + public boolean isSelectingOrigin() { + return selectingOrigin; + } + + public void finishSelectingOrigin() { + selectingOrigin = false; + } + + public void decrementRow() { + if (selectingOrigin) + setTop(top - 1); + else + setBottom(bottom - 1); + } + + public void incrementRow() { + if (selectingOrigin) + setTop(top + 1); + else + setBottom(bottom + 1); + } + + public void setRow(int row) { + if (selectingOrigin) + setTop(row); + else + setBottom(row); + } + + private void setTop(int top) { + this.top = bottom = checkBounds(top, maxRows); + } + + public int getTop() { + return Math.min(top, bottom); + } + + private void setBottom(int bottom) { + this.bottom = checkBounds(bottom, maxRows); + } + + public int getBottom() { + return Math.max(top, bottom); + } + + public void decrementColumn() { + if (selectingOrigin) + setLeft(left - 1); + else + setRight(right - 1); + } + + public void incrementColumn() { + if (selectingOrigin) + setLeft(left + 1); + else + setRight(right + 1); + } + + public void setColumn(int column) { + if (selectingOrigin) + setLeft(column); + else + setRight(column); + } + + private void setLeft(int left) { + this.left = right = checkBounds(left, maxColumns); + } + + public int getLeft() { + return Math.min(left, right); + } + + private void setRight(int right) { + this.right = checkBounds(right, maxColumns); + } + + public int getRight() { + return Math.max(left, right); + } + + public String copyFrom(VDUBuffer vb) { + int size = (getRight() - getLeft() + 1) * (getBottom() - getTop() + 1); + + StringBuffer buffer = new StringBuffer(size); + + for(int y = getTop(); y <= getBottom(); y++) { + int lastNonSpace = buffer.length(); + + for (int x = getLeft(); x <= getRight(); x++) { + // only copy printable chars + char c = vb.getChar(x, y); + + if (!Character.isDefined(c) || + (Character.isISOControl(c) && c != '\t')) + c = ' '; + + if (c != ' ') + lastNonSpace = buffer.length(); + + buffer.append(c); + } + + // Don't leave a bunch of spaces in our copy buffer. + if (buffer.length() > lastNonSpace) + buffer.delete(lastNonSpace + 1, buffer.length()); + + if (y != bottom) + buffer.append("\n"); + } + + return buffer.toString(); + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + + buffer.append("SelectionArea[top="); + buffer.append(top); + buffer.append(", bottom="); + buffer.append(bottom); + buffer.append(", left="); + buffer.append(left); + buffer.append(", right="); + buffer.append(right); + buffer.append(", maxColumns="); + buffer.append(maxColumns); + buffer.append(", maxRows="); + buffer.append(maxRows); + buffer.append(", isSelectingOrigin="); + buffer.append(isSelectingOrigin()); + buffer.append("]"); + + return buffer.toString(); + } +} diff --git a/app/src/main/java/org/connectbot/service/BackupAgent.java b/app/src/main/java/org/connectbot/service/BackupAgent.java new file mode 100644 index 0000000..1e3bd81 --- /dev/null +++ b/app/src/main/java/org/connectbot/service/BackupAgent.java @@ -0,0 +1,73 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2010 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import java.io.IOException; + +import org.connectbot.util.HostDatabase; +import org.connectbot.util.PreferenceConstants; +import org.connectbot.util.PubkeyDatabase; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.app.backup.FileBackupHelper; +import android.app.backup.SharedPreferencesBackupHelper; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +/** + * @author kroot + * + */ +public class BackupAgent extends BackupAgentHelper { + @Override + public void onCreate() { + Log.d("ConnectBot.BackupAgent", "onCreate called"); + + SharedPreferencesBackupHelper prefs = new SharedPreferencesBackupHelper(this, getPackageName() + "_preferences"); + addHelper(PreferenceConstants.BACKUP_PREF_KEY, prefs); + + FileBackupHelper hosts = new FileBackupHelper(this, "../databases/" + HostDatabase.DB_NAME); + addHelper(HostDatabase.DB_NAME, hosts); + + FileBackupHelper pubkeys = new FileBackupHelper(this, "../databases/" + PubkeyDatabase.DB_NAME); + addHelper(PubkeyDatabase.DB_NAME, pubkeys); + + } + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + synchronized (HostDatabase.dbLock) { + super.onBackup(oldState, data, newState); + } + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + Log.d("ConnectBot.BackupAgent", "onRestore called"); + + synchronized (HostDatabase.dbLock) { + Log.d("ConnectBot.BackupAgent", "onRestore in-lock"); + + super.onRestore(data, appVersionCode, newState); + } + } +} diff --git a/app/src/main/java/org/connectbot/service/BackupWrapper.java b/app/src/main/java/org/connectbot/service/BackupWrapper.java new file mode 100644 index 0000000..bfc7535 --- /dev/null +++ b/app/src/main/java/org/connectbot/service/BackupWrapper.java @@ -0,0 +1,71 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2010 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import org.connectbot.util.PreferenceConstants; + +import android.app.backup.BackupManager; +import android.content.Context; + +/** + * @author kroot + * + */ +public abstract class BackupWrapper { + public static BackupWrapper getInstance() { + if (PreferenceConstants.PRE_FROYO) + return PreFroyo.Holder.sInstance; + else + return FroyoAndBeyond.Holder.sInstance; + } + + public abstract void onDataChanged(Context context); + + private static class PreFroyo extends BackupWrapper { + private static class Holder { + private static final PreFroyo sInstance = new PreFroyo(); + } + + @Override + public void onDataChanged(Context context) { + // do nothing for now + } + } + + private static class FroyoAndBeyond extends BackupWrapper { + private static class Holder { + private static final FroyoAndBeyond sInstance = new FroyoAndBeyond(); + } + + private static BackupManager mBackupManager; + + @Override + public void onDataChanged(Context context) { + checkBackupManager(context); + if (mBackupManager != null) { + mBackupManager.dataChanged(); + } + } + + private void checkBackupManager(Context context) { + if (mBackupManager == null) { + mBackupManager = new BackupManager(context); + } + } + } +} diff --git a/app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java b/app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java new file mode 100644 index 0000000..21c41d1 --- /dev/null +++ b/app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java @@ -0,0 +1,22 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +public interface BridgeDisconnectedListener { + public void onDisconnected(TerminalBridge bridge); +} diff --git a/app/src/main/java/org/connectbot/service/ConnectionNotifier.java b/app/src/main/java/org/connectbot/service/ConnectionNotifier.java new file mode 100644 index 0000000..d276761 --- /dev/null +++ b/app/src/main/java/org/connectbot/service/ConnectionNotifier.java @@ -0,0 +1,192 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2010 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.connectbot.ConsoleActivity; +import org.connectbot.R; +import org.connectbot.bean.HostBean; +import org.connectbot.util.HostDatabase; +import org.connectbot.util.PreferenceConstants; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Color; + +/** + * @author Kenny Root + * + * Based on the concept from jasta's blog post. + */ +public abstract class ConnectionNotifier { + private static final int ONLINE_NOTIFICATION = 1; + private static final int ACTIVITY_NOTIFICATION = 2; + + public static ConnectionNotifier getInstance() { + if (PreferenceConstants.PRE_ECLAIR) + return PreEclair.Holder.sInstance; + else + return EclairAndBeyond.Holder.sInstance; + } + + protected NotificationManager getNotificationManager(Context context) { + return (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + protected Notification newNotification(Context context) { + Notification notification = new Notification(); + notification.icon = R.drawable.notification_icon; + notification.when = System.currentTimeMillis(); + + return notification; + } + + protected Notification newActivityNotification(Context context, HostBean host) { + Notification notification = newNotification(context); + + Resources res = context.getResources(); + + String contentText = res.getString( + R.string.notification_text, host.getNickname()); + + Intent notificationIntent = new Intent(context, ConsoleActivity.class); + notificationIntent.setAction("android.intent.action.VIEW"); + notificationIntent.setData(host.getUri()); + + PendingIntent contentIntent = PendingIntent.getActivity(context, 0, + notificationIntent, 0); + + notification.setLatestEventInfo(context, res.getString(R.string.app_name), contentText, contentIntent); + + notification.flags = Notification.FLAG_AUTO_CANCEL; + + notification.flags |= Notification.DEFAULT_LIGHTS; + if (HostDatabase.COLOR_RED.equals(host.getColor())) + notification.ledARGB = Color.RED; + else if (HostDatabase.COLOR_GREEN.equals(host.getColor())) + notification.ledARGB = Color.GREEN; + else if (HostDatabase.COLOR_BLUE.equals(host.getColor())) + notification.ledARGB = Color.BLUE; + else + notification.ledARGB = Color.WHITE; + notification.ledOnMS = 300; + notification.ledOffMS = 1000; + notification.flags |= Notification.FLAG_SHOW_LIGHTS; + + return notification; + } + + protected Notification newRunningNotification(Context context) { + Notification notification = newNotification(context); + + notification.flags = Notification.FLAG_ONGOING_EVENT + | Notification.FLAG_NO_CLEAR; + notification.when = 0; + + notification.contentIntent = PendingIntent.getActivity(context, + ONLINE_NOTIFICATION, + new Intent(context, ConsoleActivity.class), 0); + + Resources res = context.getResources(); + + notification.setLatestEventInfo(context, + res.getString(R.string.app_name), + res.getString(R.string.app_is_running), + notification.contentIntent); + + return notification; + } + + public void showActivityNotification(Service context, HostBean host) { + getNotificationManager(context).notify(ACTIVITY_NOTIFICATION, newActivityNotification(context, host)); + } + + public void hideActivityNotification(Service context) { + getNotificationManager(context).cancel(ACTIVITY_NOTIFICATION); + } + + public abstract void showRunningNotification(Service context); + public abstract void hideRunningNotification(Service context); + + private static class PreEclair extends ConnectionNotifier { + private static final Class[] setForegroundSignature = new Class[] {boolean.class}; + private Method setForeground = null; + + private static class Holder { + private static final PreEclair sInstance = new PreEclair(); + } + + public PreEclair() { + try { + setForeground = Service.class.getMethod("setForeground", setForegroundSignature); + } catch (Exception e) { + } + } + + @Override + public void showRunningNotification(Service context) { + if (setForeground != null) { + Object[] setForegroundArgs = new Object[1]; + setForegroundArgs[0] = Boolean.TRUE; + try { + setForeground.invoke(context, setForegroundArgs); + } catch (InvocationTargetException e) { + } catch (IllegalAccessException e) { + } + getNotificationManager(context).notify(ONLINE_NOTIFICATION, newRunningNotification(context)); + } + } + + @Override + public void hideRunningNotification(Service context) { + if (setForeground != null) { + Object[] setForegroundArgs = new Object[1]; + setForegroundArgs[0] = Boolean.FALSE; + try { + setForeground.invoke(context, setForegroundArgs); + } catch (InvocationTargetException e) { + } catch (IllegalAccessException e) { + } + getNotificationManager(context).cancel(ONLINE_NOTIFICATION); + } + } + } + + private static class EclairAndBeyond extends ConnectionNotifier { + private static class Holder { + private static final EclairAndBeyond sInstance = new EclairAndBeyond(); + } + + @Override + public void showRunningNotification(Service context) { + context.startForeground(ONLINE_NOTIFICATION, newRunningNotification(context)); + } + + @Override + public void hideRunningNotification(Service context) { + context.stopForeground(true); + } + } +} diff --git a/app/src/main/java/org/connectbot/service/ConnectivityReceiver.java b/app/src/main/java/org/connectbot/service/ConnectivityReceiver.java new file mode 100644 index 0000000..3248a2a --- /dev/null +++ b/app/src/main/java/org/connectbot/service/ConnectivityReceiver.java @@ -0,0 +1,154 @@ +/** + * + */ +package org.connectbot.service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.NetworkInfo.State; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import android.util.Log; + +/** + * @author kroot + * + */ +public class ConnectivityReceiver extends BroadcastReceiver { + private static final String TAG = "ConnectBot.ConnectivityManager"; + + private boolean mIsConnected = false; + + final private TerminalManager mTerminalManager; + + final private WifiLock mWifiLock; + + private int mNetworkRef = 0; + + private boolean mLockingWifi; + + private Object[] mLock = new Object[0]; + + public ConnectivityReceiver(TerminalManager manager, boolean lockingWifi) { + mTerminalManager = manager; + + final ConnectivityManager cm = + (ConnectivityManager) manager.getSystemService(Context.CONNECTIVITY_SERVICE); + + final WifiManager wm = (WifiManager) manager.getSystemService(Context.WIFI_SERVICE); + mWifiLock = wm.createWifiLock(TAG); + + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info != null) { + mIsConnected = (info.getState() == State.CONNECTED); + } + + mLockingWifi = lockingWifi; + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + manager.registerReceiver(this, filter); + } + + /* (non-Javadoc) + * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent) + */ + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + + if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + Log.w(TAG, "onReceived() called: " + intent); + return; + } + + boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + boolean isFailover = intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false); + + Log.d(TAG, "onReceived() called; noConnectivity? " + noConnectivity + "; isFailover? " + isFailover); + + if (noConnectivity && !isFailover && mIsConnected) { + mIsConnected = false; + mTerminalManager.onConnectivityLost(); + } else if (!mIsConnected) { + NetworkInfo info = (NetworkInfo) intent.getExtras() + .get(ConnectivityManager.EXTRA_NETWORK_INFO); + + if (mIsConnected = (info.getState() == State.CONNECTED)) { + mTerminalManager.onConnectivityRestored(); + } + } + } + + /** + * + */ + public void cleanup() { + if (mWifiLock.isHeld()) + mWifiLock.release(); + + mTerminalManager.unregisterReceiver(this); + } + + /** + * Increase the number of things using the network. Acquire a Wi-Fi lock + * if necessary. + */ + public void incRef() { + synchronized (mLock) { + mNetworkRef += 1; + + acquireWifiLockIfNecessaryLocked(); + } + } + + /** + * Decrease the number of things using the network. Release the Wi-Fi lock + * if necessary. + */ + public void decRef() { + synchronized (mLock) { + mNetworkRef -= 1; + + releaseWifiLockIfNecessaryLocked(); + } + } + + /** + * @param mLockingWifi + */ + public void setWantWifiLock(boolean lockingWifi) { + synchronized (mLock) { + mLockingWifi = lockingWifi; + + if (mLockingWifi) { + acquireWifiLockIfNecessaryLocked(); + } else { + releaseWifiLockIfNecessaryLocked(); + } + } + } + + private void acquireWifiLockIfNecessaryLocked() { + if (mLockingWifi && mNetworkRef > 0 && !mWifiLock.isHeld()) { + mWifiLock.acquire(); + } + } + + private void releaseWifiLockIfNecessaryLocked() { + if (mNetworkRef == 0 && mWifiLock.isHeld()) { + mWifiLock.release(); + } + } + + /** + * @return whether we're connected to a network + */ + public boolean isConnected() { + return mIsConnected; + } +} diff --git a/app/src/main/java/org/connectbot/service/FontSizeChangedListener.java b/app/src/main/java/org/connectbot/service/FontSizeChangedListener.java new file mode 100644 index 0000000..eb1c33d --- /dev/null +++ b/app/src/main/java/org/connectbot/service/FontSizeChangedListener.java @@ -0,0 +1,31 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +/** + * @author Kenny Root + * + */ +public interface FontSizeChangedListener { + + /** + * @param size + * new font size + */ + void onFontSizeChanged(float size); +} diff --git a/app/src/main/java/org/connectbot/service/KeyEventUtil.java b/app/src/main/java/org/connectbot/service/KeyEventUtil.java new file mode 100644 index 0000000..8ad645d --- /dev/null +++ b/app/src/main/java/org/connectbot/service/KeyEventUtil.java @@ -0,0 +1,98 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2014 Torne Wuff + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.connectbot.service; + +import android.view.KeyEvent; + +public class KeyEventUtil { + static final char CONTROL_LIMIT = ' '; + static final char PRINTABLE_LIMIT = '\u007e'; + static final char[] HEX_DIGITS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + static String printableRepresentation(String source) { + if (source == null) + return null; + + final StringBuilder sb = new StringBuilder(); + final int limit = source.length(); + char[] hexbuf = null; + int pointer = 0; + + sb.append('"'); + while (pointer < limit) { + int ch = source.charAt(pointer++); + switch (ch) { + case '\0': + sb.append("\\0"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + default: + if (CONTROL_LIMIT <= ch && ch <= PRINTABLE_LIMIT) { + sb.append((char) ch); + } else { + sb.append("\\u"); + if (hexbuf == null) + hexbuf = new char[4]; + for (int offs = 4; offs > 0; ) { + hexbuf[--offs] = HEX_DIGITS[ch & 0xf]; + ch >>>= 4; + } + sb.append(hexbuf, 0, 4); + } + } + } + return sb.append('"').toString(); + } + + public static String describeKeyEvent(int keyCode, KeyEvent event) { + StringBuilder d = new StringBuilder(); + d.append("keyCode=").append(keyCode); + d.append(", keyCodeToString=").append(KeyEvent.keyCodeToString(keyCode)); + d.append(", event.toString=").append(event.toString()); + d.append(", action=").append(event.getAction()); + d.append(", characters=").append(printableRepresentation(event.getCharacters())); + d.append(", deviceId=").append(event.getDeviceId()); + d.append(", displayLabel=").append((int) event.getDisplayLabel()); + d.append(", flags=0x").append(Integer.toHexString(event.getFlags())); + d.append(", printingKey=").append(event.isPrintingKey()); + d.append(", keyCode=").append(event.getKeyCode()); + d.append(", metaState=0x").append(Integer.toHexString(event.getMetaState())); + d.append(", modifiers=0x").append(Integer.toHexString(event.getModifiers())); + d.append(", number=").append((int) event.getNumber()); + d.append(", scanCode=").append(event.getScanCode()); + d.append(", source=").append(event.getSource()); + d.append(", unicodeChar=").append(event.getUnicodeChar()); + return d.toString(); + } +} diff --git a/app/src/main/java/org/connectbot/service/PromptHelper.java b/app/src/main/java/org/connectbot/service/PromptHelper.java new file mode 100644 index 0000000..f0a37be --- /dev/null +++ b/app/src/main/java/org/connectbot/service/PromptHelper.java @@ -0,0 +1,159 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import java.util.concurrent.Semaphore; + +import android.os.Handler; +import android.os.Message; + +/** + * Helps provide a relay for prompts and responses between a possible user + * interface and some underlying service. + * + * @author jsharkey + */ +public class PromptHelper { + private final Object tag; + + private Handler handler = null; + + private Semaphore promptToken; + private Semaphore promptResponse; + + public String promptInstructions = null; + public String promptHint = null; + public Object promptRequested = null; + + private Object response = null; + + public PromptHelper(Object tag) { + this.tag = tag; + + // Threads must acquire this before they can send a prompt. + promptToken = new Semaphore(1); + + // Responses will release this semaphore. + promptResponse = new Semaphore(0); + } + + + /** + * Register a user interface handler, if available. + */ + public void setHandler(Handler handler) { + this.handler = handler; + } + + /** + * Set an incoming value from an above user interface. Will automatically + * notify any waiting requests. + */ + public void setResponse(Object value) { + response = value; + promptRequested = null; + promptInstructions = null; + promptHint = null; + promptResponse.release(); + } + + /** + * Return the internal response value just before erasing and returning it. + */ + protected Object popResponse() { + Object value = response; + response = null; + return value; + } + + + /** + * Request a prompt response from parent. This is a blocking call until user + * interface returns a value. + * Only one thread can call this at a time. cancelPrompt() will force this to + * immediately return. + */ + private Object requestPrompt(String instructions, String hint, Object type) throws InterruptedException { + Object response = null; + + promptToken.acquire(); + + try { + promptInstructions = instructions; + promptHint = hint; + promptRequested = type; + + // notify any parent watching for live events + if (handler != null) + Message.obtain(handler, -1, tag).sendToTarget(); + + // acquire lock until user passes back value + promptResponse.acquire(); + + response = popResponse(); + } finally { + promptToken.release(); + } + + return response; + } + + /** + * Request a string response from parent. This is a blocking call until user + * interface returns a value. + * @param hint prompt hint for user to answer + * @return string user has entered + */ + public String requestStringPrompt(String instructions, String hint) { + String value = null; + try { + value = (String)this.requestPrompt(instructions, hint, String.class); + } catch(Exception e) { + } + return value; + } + + /** + * Request a boolean response from parent. This is a blocking call until user + * interface returns a value. + * @param hint prompt hint for user to answer + * @return choice user has made (yes/no) + */ + public Boolean requestBooleanPrompt(String instructions, String hint) { + Boolean value = null; + try { + value = (Boolean)this.requestPrompt(instructions, hint, Boolean.class); + } catch(Exception e) { + } + return value; + } + + /** + * Cancel an in-progress prompt. + */ + public void cancelPrompt() { + if (!promptToken.tryAcquire()) { + // A thread has the token, so try to interrupt it + response = null; + promptResponse.release(); + } else { + // No threads have acquired the token + promptToken.release(); + } + } +} diff --git a/app/src/main/java/org/connectbot/service/Relay.java b/app/src/main/java/org/connectbot/service/Relay.java new file mode 100644 index 0000000..36672ec --- /dev/null +++ b/app/src/main/java/org/connectbot/service/Relay.java @@ -0,0 +1,145 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +import org.apache.harmony.niochar.charset.additional.IBM437; +import org.connectbot.transport.AbsTransport; +import org.connectbot.util.EastAsianWidth; + +import android.util.Log; +import de.mud.terminal.vt320; + +/** + * @author Kenny Root + */ +public class Relay implements Runnable { + private static final String TAG = "ConnectBot.Relay"; + + private static final int BUFFER_SIZE = 4096; + + private TerminalBridge bridge; + + private Charset currentCharset; + private CharsetDecoder decoder; + + private AbsTransport transport; + + private vt320 buffer; + + private ByteBuffer byteBuffer; + private CharBuffer charBuffer; + + private byte[] byteArray; + private char[] charArray; + + public Relay(TerminalBridge bridge, AbsTransport transport, vt320 buffer, String encoding) { + setCharset(encoding); + this.bridge = bridge; + this.transport = transport; + this.buffer = buffer; + } + + public void setCharset(String encoding) { + Log.d("ConnectBot.Relay", "changing charset to " + encoding); + Charset charset; + if (encoding.equals("CP437")) + charset = new IBM437("IBM437", + new String[] { "IBM437", "CP437" }); + else + charset = Charset.forName(encoding); + + if (charset == currentCharset || charset == null) + return; + + CharsetDecoder newCd = charset.newDecoder(); + newCd.onUnmappableCharacter(CodingErrorAction.REPLACE); + newCd.onMalformedInput(CodingErrorAction.REPLACE); + + currentCharset = charset; + synchronized (this) { + decoder = newCd; + } + } + + public Charset getCharset() { + return currentCharset; + } + + public void run() { + byteBuffer = ByteBuffer.allocate(BUFFER_SIZE); + charBuffer = CharBuffer.allocate(BUFFER_SIZE); + + /* for East Asian character widths */ + byte[] wideAttribute = new byte[BUFFER_SIZE]; + + byteArray = byteBuffer.array(); + charArray = charBuffer.array(); + + CoderResult result; + + int bytesRead = 0; + byteBuffer.limit(0); + int bytesToRead; + int offset; + int charWidth; + + EastAsianWidth measurer = EastAsianWidth.getInstance(); + + try { + while (true) { + charWidth = bridge.charWidth; + bytesToRead = byteBuffer.capacity() - byteBuffer.limit(); + offset = byteBuffer.arrayOffset() + byteBuffer.limit(); + bytesRead = transport.read(byteArray, offset, bytesToRead); + + if (bytesRead > 0) { + byteBuffer.limit(byteBuffer.limit() + bytesRead); + + synchronized (this) { + result = decoder.decode(byteBuffer, charBuffer, false); + } + + if (result.isUnderflow() && + byteBuffer.limit() == byteBuffer.capacity()) { + byteBuffer.compact(); + byteBuffer.limit(byteBuffer.position()); + byteBuffer.position(0); + } + + offset = charBuffer.position(); + + measurer.measure(charArray, 0, offset, wideAttribute, bridge.defaultPaint, charWidth); + buffer.putString(charArray, wideAttribute, 0, charBuffer.position()); + bridge.propagateConsoleText(charArray, charBuffer.position()); + charBuffer.clear(); + bridge.redraw(); + } + } + } catch (IOException e) { + Log.e(TAG, "Problem while handling incoming data in relay thread", e); + } + } +} diff --git a/app/src/main/java/org/connectbot/service/TerminalBridge.java b/app/src/main/java/org/connectbot/service/TerminalBridge.java new file mode 100644 index 0000000..6b87b74 --- /dev/null +++ b/app/src/main/java/org/connectbot/service/TerminalBridge.java @@ -0,0 +1,1018 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.connectbot.R; +import org.connectbot.TerminalView; +import org.connectbot.bean.HostBean; +import org.connectbot.bean.PortForwardBean; +import org.connectbot.bean.SelectionArea; +import org.connectbot.transport.AbsTransport; +import org.connectbot.transport.TransportFactory; +import org.connectbot.util.HostDatabase; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.FontMetrics; +import android.graphics.Typeface; +import android.text.ClipboardManager; +import android.util.Log; +import de.mud.terminal.VDUBuffer; +import de.mud.terminal.VDUDisplay; +import de.mud.terminal.vt320; + + +/** + * Provides a bridge between a MUD terminal buffer and a possible TerminalView. + * This separation allows us to keep the TerminalBridge running in a background + * service. A TerminalView shares down a bitmap that we can use for rendering + * when available. + * + * This class also provides SSH hostkey verification prompting, and password + * prompting. + */ +@SuppressWarnings("deprecation") // for ClipboardManager +public class TerminalBridge implements VDUDisplay { + public final static String TAG = "ConnectBot.TerminalBridge"; + + public final static int DEFAULT_FONT_SIZE = 10; + private final static int FONT_SIZE_STEP = 2; + + public Integer[] color; + + public int defaultFg = HostDatabase.DEFAULT_FG_COLOR; + public int defaultBg = HostDatabase.DEFAULT_BG_COLOR; + + protected final TerminalManager manager; + + public HostBean host; + + /* package */ AbsTransport transport; + + final Paint defaultPaint; + + private Relay relay; + + private final String emulation; + private final int scrollback; + + public Bitmap bitmap = null; + public VDUBuffer buffer = null; + + private TerminalView parent = null; + private final Canvas canvas = new Canvas(); + + private boolean disconnected = false; + private boolean awaitingClose = false; + + private boolean forcedSize = false; + private int columns; + private int rows; + + /* package */ final TerminalKeyListener keyListener; + + private boolean selectingForCopy = false; + private final SelectionArea selectionArea; + + // TODO add support for the new clipboard API + private ClipboardManager clipboard; + + public int charWidth = -1; + public int charHeight = -1; + private int charTop = -1; + + private float fontSize = -1; + + private final List fontSizeChangedListeners; + + private final List localOutput; + + /** + * Flag indicating if we should perform a full-screen redraw during our next + * rendering pass. + */ + private boolean fullRedraw = false; + + public PromptHelper promptHelper; + + protected BridgeDisconnectedListener disconnectListener = null; + + /** + * Create a new terminal bridge suitable for unit testing. + */ + public TerminalBridge() { + buffer = new vt320() { + @Override + public void write(byte[] b) {} + @Override + public void write(int b) {} + @Override + public void sendTelnetCommand(byte cmd) {} + @Override + public void setWindowSize(int c, int r) {} + @Override + public void debug(String s) {} + }; + + emulation = null; + manager = null; + + defaultPaint = new Paint(); + + selectionArea = new SelectionArea(); + scrollback = 1; + + localOutput = new LinkedList(); + + fontSizeChangedListeners = new LinkedList(); + + transport = null; + + keyListener = new TerminalKeyListener(manager, this, buffer, null); + } + + /** + * Create new terminal bridge with following parameters. We will immediately + * launch thread to start SSH connection and handle any hostkey verification + * and password authentication. + */ + public TerminalBridge(final TerminalManager manager, final HostBean host) throws IOException { + this.manager = manager; + this.host = host; + + emulation = manager.getEmulation(); + scrollback = manager.getScrollback(); + + // create prompt helper to relay password and hostkey requests up to gui + promptHelper = new PromptHelper(this); + + // create our default paint + defaultPaint = new Paint(); + defaultPaint.setAntiAlias(true); + defaultPaint.setTypeface(Typeface.MONOSPACE); + defaultPaint.setFakeBoldText(true); // more readable? + + localOutput = new LinkedList(); + + fontSizeChangedListeners = new LinkedList(); + + int hostFontSize = host.getFontSize(); + if (hostFontSize <= 0) + hostFontSize = DEFAULT_FONT_SIZE; + setFontSize(hostFontSize); + + // create terminal buffer and handle outgoing data + // this is probably status reply information + buffer = new vt320() { + @Override + public void debug(String s) { + Log.d(TAG, s); + } + + @Override + public void write(byte[] b) { + try { + if (b != null && transport != null) + transport.write(b); + } catch (IOException e) { + Log.e(TAG, "Problem writing outgoing data in vt320() thread", e); + } + } + + @Override + public void write(int b) { + try { + if (transport != null) + transport.write(b); + } catch (IOException e) { + Log.e(TAG, "Problem writing outgoing data in vt320() thread", e); + } + } + + // We don't use telnet sequences. + @Override + public void sendTelnetCommand(byte cmd) { + } + + // We don't want remote to resize our window. + @Override + public void setWindowSize(int c, int r) { + } + + @Override + public void beep() { + if (parent.isShown()) + manager.playBeep(); + else + manager.sendActivityNotification(host); + } + }; + + // Don't keep any scrollback if a session is not being opened. + if (host.getWantSession()) + buffer.setBufferSize(scrollback); + else + buffer.setBufferSize(0); + + resetColors(); + buffer.setDisplay(this); + + selectionArea = new SelectionArea(); + + keyListener = new TerminalKeyListener(manager, this, buffer, host.getEncoding()); + } + + public PromptHelper getPromptHelper() { + return promptHelper; + } + + /** + * Spawn thread to open connection and start login process. + */ + protected void startConnection() { + transport = TransportFactory.getTransport(host.getProtocol()); + transport.setBridge(this); + transport.setManager(manager); + transport.setHost(host); + + // TODO make this more abstract so we don't litter on AbsTransport + transport.setCompression(host.getCompression()); + transport.setUseAuthAgent(host.getUseAuthAgent()); + transport.setEmulation(emulation); + + if (transport.canForwardPorts()) { + for (PortForwardBean portForward : manager.hostdb.getPortForwardsForHost(host)) + transport.addPortForward(portForward); + } + + outputLine(manager.res.getString(R.string.terminal_connecting, host.getHostname(), host.getPort(), host.getProtocol())); + + Thread connectionThread = new Thread(new Runnable() { + public void run() { + transport.connect(); + } + }); + connectionThread.setName("Connection"); + connectionThread.setDaemon(true); + connectionThread.start(); + } + + /** + * Handle challenges from keyboard-interactive authentication mode. + */ + public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) { + String[] responses = new String[numPrompts]; + for(int i = 0; i < numPrompts; i++) { + // request response from user for each prompt + responses[i] = promptHelper.requestStringPrompt(instruction, prompt[i]); + } + return responses; + } + + /** + * @return charset in use by bridge + */ + public Charset getCharset() { + return relay.getCharset(); + } + + /** + * Sets the encoding used by the terminal. If the connection is live, + * then the character set is changed for the next read. + * @param encoding the canonical name of the character encoding + */ + public void setCharset(String encoding) { + if (relay != null) + relay.setCharset(encoding); + keyListener.setCharset(encoding); + } + + /** + * Convenience method for writing a line into the underlying MUD buffer. + * Should never be called once the session is established. + */ + public final void outputLine(String line) { + if (transport != null && transport.isSessionOpen()) + Log.e(TAG, "Session established, cannot use outputLine!", new IOException("outputLine call traceback")); + + synchronized (localOutput) { + final String s = line + "\r\n"; + + localOutput.add(s); + + ((vt320) buffer).putString(s); + + // For accessibility + final char[] charArray = s.toCharArray(); + propagateConsoleText(charArray, charArray.length); + } + } + + /** + * Inject a specific string into this terminal. Used for post-login strings + * and pasting clipboard. + */ + public void injectString(final String string) { + if (string == null || string.length() == 0) + return; + + Thread injectStringThread = new Thread(new Runnable() { + public void run() { + try { + transport.write(string.getBytes(host.getEncoding())); + } catch (Exception e) { + Log.e(TAG, "Couldn't inject string to remote host: ", e); + } + } + }); + injectStringThread.setName("InjectString"); + injectStringThread.start(); + } + + /** + * Internal method to request actual PTY terminal once we've finished + * authentication. If called before authenticated, it will just fail. + */ + public void onConnected() { + disconnected = false; + + ((vt320) buffer).reset(); + + // We no longer need our local output. + localOutput.clear(); + + // previously tried vt100 and xterm for emulation modes + // "screen" works the best for color and escape codes + ((vt320) buffer).setAnswerBack(emulation); + + if (HostDatabase.DELKEY_BACKSPACE.equals(host.getDelKey())) + ((vt320) buffer).setBackspace(vt320.DELETE_IS_BACKSPACE); + else + ((vt320) buffer).setBackspace(vt320.DELETE_IS_DEL); + + // create thread to relay incoming connection data to buffer + relay = new Relay(this, transport, (vt320) buffer, host.getEncoding()); + Thread relayThread = new Thread(relay); + relayThread.setDaemon(true); + relayThread.setName("Relay"); + relayThread.start(); + + // force font-size to make sure we resizePTY as needed + setFontSize(fontSize); + + // finally send any post-login string, if requested + injectString(host.getPostLogin()); + } + + /** + * @return whether a session is open or not + */ + public boolean isSessionOpen() { + if (transport != null) + return transport.isSessionOpen(); + return false; + } + + public void setOnDisconnectedListener(BridgeDisconnectedListener disconnectListener) { + this.disconnectListener = disconnectListener; + } + + /** + * Force disconnection of this terminal bridge. + */ + public void dispatchDisconnect(boolean immediate) { + // We don't need to do this multiple times. + synchronized (this) { + if (disconnected && !immediate) + return; + + disconnected = true; + } + + // Cancel any pending prompts. + promptHelper.cancelPrompt(); + + // disconnection request hangs if we havent really connected to a host yet + // temporary fix is to just spawn disconnection into a thread + Thread disconnectThread = new Thread(new Runnable() { + public void run() { + if (transport != null && transport.isConnected()) + transport.close(); + } + }); + disconnectThread.setName("Disconnect"); + disconnectThread.start(); + + if (immediate) { + awaitingClose = true; + if (disconnectListener != null) + disconnectListener.onDisconnected(TerminalBridge.this); + } else { + { + final String line = manager.res.getString(R.string.alert_disconnect_msg); + ((vt320) buffer).putString("\r\n" + line + "\r\n"); + } + if (host.getStayConnected()) { + manager.requestReconnect(this); + return; + } + Thread disconnectPromptThread = new Thread(new Runnable() { + public void run() { + Boolean result = promptHelper.requestBooleanPrompt(null, + manager.res.getString(R.string.prompt_host_disconnected)); + if (result == null || result.booleanValue()) { + awaitingClose = true; + + // Tell the TerminalManager that we can be destroyed now. + if (disconnectListener != null) + disconnectListener.onDisconnected(TerminalBridge.this); + } + } + }); + disconnectPromptThread.setName("DisconnectPrompt"); + disconnectPromptThread.setDaemon(true); + disconnectPromptThread.start(); + } + } + + public void setSelectingForCopy(boolean selectingForCopy) { + this.selectingForCopy = selectingForCopy; + } + + public boolean isSelectingForCopy() { + return selectingForCopy; + } + + public SelectionArea getSelectionArea() { + return selectionArea; + } + + public synchronized void tryKeyVibrate() { + manager.tryKeyVibrate(); + } + + /** + * Request a different font size. Will make call to parentChanged() to make + * sure we resize PTY if needed. + */ + /* package */ final void setFontSize(float size) { + if (size <= 0.0) + return; + + defaultPaint.setTextSize(size); + fontSize = size; + + // read new metrics to get exact pixel dimensions + FontMetrics fm = defaultPaint.getFontMetrics(); + charTop = (int)Math.ceil(fm.top); + + float[] widths = new float[1]; + defaultPaint.getTextWidths("X", widths); + charWidth = (int)Math.ceil(widths[0]); + charHeight = (int)Math.ceil(fm.descent - fm.top); + + // refresh any bitmap with new font size + if(parent != null) + parentChanged(parent); + + for (FontSizeChangedListener ofscl : fontSizeChangedListeners) + ofscl.onFontSizeChanged(size); + + host.setFontSize((int) fontSize); + manager.hostdb.updateFontSize(host); + + forcedSize = false; + } + + /** + * Add an {@link FontSizeChangedListener} to the list of listeners for this + * bridge. + * + * @param listener + * listener to add + */ + public void addFontSizeChangedListener(FontSizeChangedListener listener) { + fontSizeChangedListeners.add(listener); + } + + /** + * Remove an {@link FontSizeChangedListener} from the list of listeners for + * this bridge. + * + * @param listener + */ + public void removeFontSizeChangedListener(FontSizeChangedListener listener) { + fontSizeChangedListeners.remove(listener); + } + + /** + * Something changed in our parent {@link TerminalView}, maybe it's a new + * parent, or maybe it's an updated font size. We should recalculate + * terminal size information and request a PTY resize. + */ + public final synchronized void parentChanged(TerminalView parent) { + if (manager != null && !manager.isResizeAllowed()) { + Log.d(TAG, "Resize is not allowed now"); + return; + } + + this.parent = parent; + final int width = parent.getWidth(); + final int height = parent.getHeight(); + + // Something has gone wrong with our layout; we're 0 width or height! + if (width <= 0 || height <= 0) + return; + + clipboard = (ClipboardManager) parent.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + keyListener.setClipboardManager(clipboard); + + if (!forcedSize) { + // recalculate buffer size + int newColumns, newRows; + + newColumns = width / charWidth; + newRows = height / charHeight; + + // If nothing has changed in the terminal dimensions and not an intial + // draw then don't blow away scroll regions and such. + if (newColumns == columns && newRows == rows) + return; + + columns = newColumns; + rows = newRows; + } + + // reallocate new bitmap if needed + boolean newBitmap = (bitmap == null); + if(bitmap != null) + newBitmap = (bitmap.getWidth() != width || bitmap.getHeight() != height); + + if (newBitmap) { + discardBitmap(); + bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + canvas.setBitmap(bitmap); + } + + // clear out any old buffer information + defaultPaint.setColor(Color.BLACK); + canvas.drawPaint(defaultPaint); + + // Stroke the border of the terminal if the size is being forced; + if (forcedSize) { + int borderX = (columns * charWidth) + 1; + int borderY = (rows * charHeight) + 1; + + defaultPaint.setColor(Color.GRAY); + defaultPaint.setStrokeWidth(0.0f); + if (width >= borderX) + canvas.drawLine(borderX, 0, borderX, borderY + 1, defaultPaint); + if (height >= borderY) + canvas.drawLine(0, borderY, borderX + 1, borderY, defaultPaint); + } + + try { + // request a terminal pty resize + synchronized (buffer) { + buffer.setScreenSize(columns, rows, true); + } + + if(transport != null) + transport.setDimensions(columns, rows, width, height); + } catch(Exception e) { + Log.e(TAG, "Problem while trying to resize screen or PTY", e); + } + + // redraw local output if we don't have a sesson to receive our resize request + if (transport == null) { + synchronized (localOutput) { + ((vt320) buffer).reset(); + + for (String line : localOutput) + ((vt320) buffer).putString(line); + } + } + + // force full redraw with new buffer size + fullRedraw = true; + redraw(); + + parent.notifyUser(String.format("%d x %d", columns, rows)); + + Log.i(TAG, String.format("parentChanged() now width=%d, height=%d", columns, rows)); + } + + /** + * Somehow our parent {@link TerminalView} was destroyed. Now we don't need + * to redraw anywhere, and we can recycle our internal bitmap. + */ + public synchronized void parentDestroyed() { + parent = null; + discardBitmap(); + } + + private void discardBitmap() { + if (bitmap != null) + bitmap.recycle(); + bitmap = null; + } + + public void setVDUBuffer(VDUBuffer buffer) { + this.buffer = buffer; + } + + public VDUBuffer getVDUBuffer() { + return buffer; + } + + public void propagateConsoleText(char[] rawText, int length) { + if (parent != null) { + parent.propagateConsoleText(rawText, length); + } + } + + public void onDraw() { + int fg, bg; + synchronized (buffer) { + boolean entireDirty = buffer.update[0] || fullRedraw; + boolean isWideCharacter = false; + + // walk through all lines in the buffer + for(int l = 0; l < buffer.height; l++) { + + // check if this line is dirty and needs to be repainted + // also check for entire-buffer dirty flags + if (!entireDirty && !buffer.update[l + 1]) continue; + + // reset dirty flag for this line + buffer.update[l + 1] = false; + + // walk through all characters in this line + for (int c = 0; c < buffer.width; c++) { + int addr = 0; + int currAttr = buffer.charAttributes[buffer.windowBase + l][c]; + + { + int fgcolor = defaultFg; + + // check if foreground color attribute is set + if ((currAttr & VDUBuffer.COLOR_FG) != 0) + fgcolor = ((currAttr & VDUBuffer.COLOR_FG) >> VDUBuffer.COLOR_FG_SHIFT) - 1; + + if (fgcolor < 8 && (currAttr & VDUBuffer.BOLD) != 0) + fg = color[fgcolor + 8]; + else + fg = color[fgcolor]; + } + + // check if background color attribute is set + if ((currAttr & VDUBuffer.COLOR_BG) != 0) + bg = color[((currAttr & VDUBuffer.COLOR_BG) >> VDUBuffer.COLOR_BG_SHIFT) - 1]; + else + bg = color[defaultBg]; + + // support character inversion by swapping background and foreground color + if ((currAttr & VDUBuffer.INVERT) != 0) { + int swapc = bg; + bg = fg; + fg = swapc; + } + + // set underlined attributes if requested + defaultPaint.setUnderlineText((currAttr & VDUBuffer.UNDERLINE) != 0); + + isWideCharacter = (currAttr & VDUBuffer.FULLWIDTH) != 0; + + if (isWideCharacter) + addr++; + else { + // determine the amount of continuous characters with the same settings and print them all at once + while(c + addr < buffer.width + && buffer.charAttributes[buffer.windowBase + l][c + addr] == currAttr) { + addr++; + } + } + + // Save the current clip region + canvas.save(Canvas.CLIP_SAVE_FLAG); + + // clear this dirty area with background color + defaultPaint.setColor(bg); + if (isWideCharacter) { + canvas.clipRect(c * charWidth, + l * charHeight, + (c + 2) * charWidth, + (l + 1) * charHeight); + } else { + canvas.clipRect(c * charWidth, + l * charHeight, + (c + addr) * charWidth, + (l + 1) * charHeight); + } + canvas.drawPaint(defaultPaint); + + // write the text string starting at 'c' for 'addr' number of characters + defaultPaint.setColor(fg); + if((currAttr & VDUBuffer.INVISIBLE) == 0) + canvas.drawText(buffer.charArray[buffer.windowBase + l], c, + addr, c * charWidth, (l * charHeight) - charTop, + defaultPaint); + + // Restore the previous clip region + canvas.restore(); + + // advance to the next text block with different characteristics + c += addr - 1; + if (isWideCharacter) + c++; + } + } + + // reset entire-buffer flags + buffer.update[0] = false; + } + fullRedraw = false; + } + + public void redraw() { + if (parent != null) + parent.postInvalidate(); + } + + // We don't have a scroll bar. + public void updateScrollBar() { + } + + /** + * Resize terminal to fit [rows]x[cols] in screen of size [width]x[height] + * @param rows + * @param cols + * @param width + * @param height + */ + public synchronized void resizeComputed(int cols, int rows, int width, int height) { + float size = 8.0f; + float step = 8.0f; + float limit = 0.125f; + + int direction; + + while ((direction = fontSizeCompare(size, cols, rows, width, height)) < 0) + size += step; + + if (direction == 0) { + Log.d("fontsize", String.format("Found match at %f", size)); + return; + } + + step /= 2.0f; + size -= step; + + while ((direction = fontSizeCompare(size, cols, rows, width, height)) != 0 + && step >= limit) { + step /= 2.0f; + if (direction > 0) { + size -= step; + } else { + size += step; + } + } + + if (direction > 0) + size -= step; + + this.columns = cols; + this.rows = rows; + setFontSize(size); + forcedSize = true; + } + + private int fontSizeCompare(float size, int cols, int rows, int width, int height) { + // read new metrics to get exact pixel dimensions + defaultPaint.setTextSize(size); + FontMetrics fm = defaultPaint.getFontMetrics(); + + float[] widths = new float[1]; + defaultPaint.getTextWidths("X", widths); + int termWidth = (int)widths[0] * cols; + int termHeight = (int)Math.ceil(fm.descent - fm.top) * rows; + + Log.d("fontsize", String.format("font size %f resulted in %d x %d", size, termWidth, termHeight)); + + // Check to see if it fits in resolution specified. + if (termWidth > width || termHeight > height) + return 1; + + if (termWidth == width || termHeight == height) + return 0; + + return -1; + } + + /** + * @return whether underlying transport can forward ports + */ + public boolean canFowardPorts() { + return transport.canForwardPorts(); + } + + /** + * Adds the {@link PortForwardBean} to the list. + * @param portForward the port forward bean to add + * @return true on successful addition + */ + public boolean addPortForward(PortForwardBean portForward) { + return transport.addPortForward(portForward); + } + + /** + * Removes the {@link PortForwardBean} from the list. + * @param portForward the port forward bean to remove + * @return true on successful removal + */ + public boolean removePortForward(PortForwardBean portForward) { + return transport.removePortForward(portForward); + } + + /** + * @return the list of port forwards + */ + public List getPortForwards() { + return transport.getPortForwards(); + } + + /** + * Enables a port forward member. After calling this method, the port forward should + * be operational. + * @param portForward member of our current port forwards list to enable + * @return true on successful port forward setup + */ + public boolean enablePortForward(PortForwardBean portForward) { + if (!transport.isConnected()) { + Log.i(TAG, "Attempt to enable port forward while not connected"); + return false; + } + + return transport.enablePortForward(portForward); + } + + /** + * Disables a port forward member. After calling this method, the port forward should + * be non-functioning. + * @param portForward member of our current port forwards list to enable + * @return true on successful port forward tear-down + */ + public boolean disablePortForward(PortForwardBean portForward) { + if (!transport.isConnected()) { + Log.i(TAG, "Attempt to disable port forward while not connected"); + return false; + } + + return transport.disablePortForward(portForward); + } + + /** + * @return whether the TerminalBridge should close + */ + public boolean isAwaitingClose() { + return awaitingClose; + } + + /** + * @return whether this connection had started and subsequently disconnected + */ + public boolean isDisconnected() { + return disconnected; + } + + /* (non-Javadoc) + * @see de.mud.terminal.VDUDisplay#setColor(byte, byte, byte, byte) + */ + public void setColor(int index, int red, int green, int blue) { + // Don't allow the system colors to be overwritten for now. May violate specs. + if (index < color.length && index >= 16) + color[index] = 0xff000000 | red << 16 | green << 8 | blue; + } + + public final void resetColors() { + int[] defaults = manager.hostdb.getDefaultColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME); + defaultFg = defaults[0]; + defaultBg = defaults[1]; + + color = manager.hostdb.getColorsForScheme(HostDatabase.DEFAULT_COLOR_SCHEME); + } + + private static Pattern urlPattern = null; + + /** + * @return + */ + public List scanForURLs() { + List urls = new LinkedList(); + + if (urlPattern == null) { + // based on http://www.ietf.org/rfc/rfc2396.txt + String scheme = "[A-Za-z][-+.0-9A-Za-z]*"; + String unreserved = "[-._~0-9A-Za-z]"; + String pctEncoded = "%[0-9A-Fa-f]{2}"; + String subDelims = "[!$&'()*+,;:=]"; + String userinfo = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + "|:)*"; + String h16 = "[0-9A-Fa-f]{1,4}"; + String decOctet = "(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])"; + String ipv4address = decOctet + "\\." + decOctet + "\\." + decOctet + "\\." + decOctet; + String ls32 = "(?:" + h16 + ":" + h16 + "|" + ipv4address + ")"; + String ipv6address = "(?:(?:" + h16 + "){6}" + ls32 + ")"; + String ipvfuture = "v[0-9A-Fa-f]+.(?:" + unreserved + "|" + subDelims + "|:)+"; + String ipLiteral = "\\[(?:" + ipv6address + "|" + ipvfuture + ")\\]"; + String regName = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + ")*"; + String host = "(?:" + ipLiteral + "|" + ipv4address + "|" + regName + ")"; + String port = "[0-9]*"; + String authority = "(?:" + userinfo + "@)?" + host + "(?::" + port + ")?"; + String pchar = "(?:" + unreserved + "|" + pctEncoded + "|" + subDelims + "|@)"; + String segment = pchar + "*"; + String pathAbempty = "(?:/" + segment + ")*"; + String segmentNz = pchar + "+"; + String pathAbsolute = "/(?:" + segmentNz + "(?:/" + segment + ")*)?"; + String pathRootless = segmentNz + "(?:/" + segment + ")*"; + String hierPart = "(?://" + authority + pathAbempty + "|" + pathAbsolute + "|" + pathRootless + ")"; + String query = "(?:" + pchar + "|/|\\?)*"; + String fragment = "(?:" + pchar + "|/|\\?)*"; + String uriRegex = scheme + ":" + hierPart + "(?:" + query + ")?(?:#" + fragment + ")?"; + urlPattern = Pattern.compile(uriRegex); + } + + char[] visibleBuffer = new char[buffer.height * buffer.width]; + for (int l = 0; l < buffer.height; l++) + System.arraycopy(buffer.charArray[buffer.windowBase + l], 0, + visibleBuffer, l * buffer.width, buffer.width); + + Matcher urlMatcher = urlPattern.matcher(new String(visibleBuffer)); + while (urlMatcher.find()) + urls.add(urlMatcher.group()); + + return urls; + } + + /** + * @return + */ + public boolean isUsingNetwork() { + return transport.usesNetwork(); + } + + /** + * @return + */ + public TerminalKeyListener getKeyHandler() { + return keyListener; + } + + /** + * + */ + public void resetScrollPosition() { + // if we're in scrollback, scroll to bottom of window on input + if (buffer.windowBase != buffer.screenBase) + buffer.setWindowBase(buffer.screenBase); + } + + /** + * + */ + public void increaseFontSize() { + setFontSize(fontSize + FONT_SIZE_STEP); + } + + /** + * + */ + public void decreaseFontSize() { + setFontSize(fontSize - FONT_SIZE_STEP); + } +} diff --git a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java new file mode 100644 index 0000000..7ff21df --- /dev/null +++ b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java @@ -0,0 +1,558 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2010 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.connectbot.service; + +import java.io.IOException; + +import org.connectbot.TerminalView; +import org.connectbot.bean.SelectionArea; +import org.connectbot.util.PreferenceConstants; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.Configuration; +import android.preference.PreferenceManager; +import android.text.ClipboardManager; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnKeyListener; +import de.mud.terminal.VDUBuffer; +import de.mud.terminal.vt320; + +/** + * @author kenny + * + */ +@SuppressWarnings("deprecation") // for ClipboardManager +public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceChangeListener { + private static final String TAG = "ConnectBot.OnKeyListener"; + + // Constants for our private tracking of modifier state + public final static int OUR_CTRL_ON = 0x01; + public final static int OUR_CTRL_LOCK = 0x02; + public final static int OUR_ALT_ON = 0x04; + public final static int OUR_ALT_LOCK = 0x08; + public final static int OUR_SHIFT_ON = 0x10; + public final static int OUR_SHIFT_LOCK = 0x20; + private final static int OUR_SLASH = 0x40; + private final static int OUR_TAB = 0x80; + + // All the transient key codes + private final static int OUR_TRANSIENT = OUR_CTRL_ON | OUR_ALT_ON + | OUR_SHIFT_ON | OUR_SLASH | OUR_TAB; + + // The bit mask of momentary and lock states for each + private final static int OUR_CTRL_MASK = OUR_CTRL_ON | OUR_CTRL_LOCK; + private final static int OUR_ALT_MASK = OUR_ALT_ON | OUR_ALT_LOCK; + private final static int OUR_SHIFT_MASK = OUR_SHIFT_ON | OUR_SHIFT_LOCK; + + // backport constants from api level 11 + private final static int KEYCODE_ESCAPE = 111; + private final static int HC_META_CTRL_ON = 0x1000; + private final static int HC_META_CTRL_LEFT_ON = 0x2000; + private final static int HC_META_CTRL_RIGHT_ON = 0x4000; + private final static int HC_META_CTRL_MASK = HC_META_CTRL_ON | HC_META_CTRL_RIGHT_ON + | HC_META_CTRL_LEFT_ON; + private final static int HC_META_ALT_MASK = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON + | KeyEvent.META_ALT_RIGHT_ON; + + private final TerminalManager manager; + private final TerminalBridge bridge; + private final VDUBuffer buffer; + + private String keymode = null; + private final boolean deviceHasHardKeyboard; + private boolean shiftedNumbersAreFKeysOnHardKeyboard; + private boolean controlNumbersAreFKeysOnSoftKeyboard; + private boolean volumeKeysChangeFontSize; + + private int ourMetaState = 0; + + private int mDeadKey = 0; + + // TODO add support for the new API. + private ClipboardManager clipboard = null; + + private boolean selectingForCopy = false; + private final SelectionArea selectionArea; + + private String encoding; + + private final SharedPreferences prefs; + + public TerminalKeyListener(TerminalManager manager, + TerminalBridge bridge, + VDUBuffer buffer, + String encoding) { + this.manager = manager; + this.bridge = bridge; + this.buffer = buffer; + this.encoding = encoding; + + selectionArea = new SelectionArea(); + + prefs = PreferenceManager.getDefaultSharedPreferences(manager); + prefs.registerOnSharedPreferenceChangeListener(this); + + deviceHasHardKeyboard = (manager.res.getConfiguration().keyboard + == Configuration.KEYBOARD_QWERTY); + + updatePrefs(); + } + + /** + * Handle onKey() events coming down from a {@link TerminalView} above us. + * Modify the keys to make more sense to a host then pass it to the transport. + */ + public boolean onKey(View v, int keyCode, KeyEvent event) { + try { + // skip keys if we aren't connected yet or have been disconnected + if (bridge.isDisconnected() || bridge.transport == null) + return false; + + final boolean interpretAsHardKeyboard = deviceHasHardKeyboard && + !manager.hardKeyboardHidden; + final boolean rightModifiersAreSlashAndTab = interpretAsHardKeyboard && + PreferenceConstants.KEYMODE_RIGHT.equals(keymode); + final boolean leftModifiersAreSlashAndTab = interpretAsHardKeyboard && + PreferenceConstants.KEYMODE_LEFT.equals(keymode); + final boolean shiftedNumbersAreFKeys = shiftedNumbersAreFKeysOnHardKeyboard && + interpretAsHardKeyboard; + final boolean controlNumbersAreFKeys = controlNumbersAreFKeysOnSoftKeyboard && + !interpretAsHardKeyboard; + + // Ignore all key-up events except for the special keys + if (event.getAction() == KeyEvent.ACTION_UP) { + if (rightModifiersAreSlashAndTab) { + if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT + && (ourMetaState & OUR_SLASH) != 0) { + ourMetaState &= ~OUR_TRANSIENT; + bridge.transport.write('/'); + return true; + } else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT + && (ourMetaState & OUR_TAB) != 0) { + ourMetaState &= ~OUR_TRANSIENT; + bridge.transport.write(0x09); + return true; + } + } else if (leftModifiersAreSlashAndTab) { + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT + && (ourMetaState & OUR_SLASH) != 0) { + ourMetaState &= ~OUR_TRANSIENT; + bridge.transport.write('/'); + return true; + } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + && (ourMetaState & OUR_TAB) != 0) { + ourMetaState &= ~OUR_TRANSIENT; + bridge.transport.write(0x09); + return true; + } + } + + return false; + } + + //Log.i("CBKeyDebug", KeyEventUtil.describeKeyEvent(keyCode, event)); + + if (volumeKeysChangeFontSize) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + bridge.increaseFontSize(); + return true; + } else if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + bridge.decreaseFontSize(); + return true; + } + } + + bridge.resetScrollPosition(); + + // Handle potentially multi-character IME input. + if (keyCode == KeyEvent.KEYCODE_UNKNOWN && + event.getAction() == KeyEvent.ACTION_MULTIPLE) { + byte[] input = event.getCharacters().getBytes(encoding); + bridge.transport.write(input); + return true; + } + + /// Handle alt and shift keys if they aren't repeating + if (event.getRepeatCount() == 0) { + if (rightModifiersAreSlashAndTab) { + switch (keyCode) { + case KeyEvent.KEYCODE_ALT_RIGHT: + ourMetaState |= OUR_SLASH; + return true; + case KeyEvent.KEYCODE_SHIFT_RIGHT: + ourMetaState |= OUR_TAB; + return true; + case KeyEvent.KEYCODE_SHIFT_LEFT: + metaPress(OUR_SHIFT_ON); + return true; + case KeyEvent.KEYCODE_ALT_LEFT: + metaPress(OUR_ALT_ON); + return true; + } + } else if (leftModifiersAreSlashAndTab) { + switch (keyCode) { + case KeyEvent.KEYCODE_ALT_LEFT: + ourMetaState |= OUR_SLASH; + return true; + case KeyEvent.KEYCODE_SHIFT_LEFT: + ourMetaState |= OUR_TAB; + return true; + case KeyEvent.KEYCODE_SHIFT_RIGHT: + metaPress(OUR_SHIFT_ON); + return true; + case KeyEvent.KEYCODE_ALT_RIGHT: + metaPress(OUR_ALT_ON); + return true; + } + } else { + switch (keyCode) { + case KeyEvent.KEYCODE_ALT_LEFT: + case KeyEvent.KEYCODE_ALT_RIGHT: + metaPress(OUR_ALT_ON); + return true; + case KeyEvent.KEYCODE_SHIFT_LEFT: + case KeyEvent.KEYCODE_SHIFT_RIGHT: + metaPress(OUR_SHIFT_ON); + return true; + } + } + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + if (selectingForCopy) { + if (selectionArea.isSelectingOrigin()) + selectionArea.finishSelectingOrigin(); + else { + if (clipboard != null) { + // copy selected area to clipboard + String copiedText = selectionArea.copyFrom(buffer); + clipboard.setText(copiedText); + // XXX STOPSHIP +// manager.notifyUser(manager.getString( +// R.string.console_copy_done, +// copiedText.length())); + selectingForCopy = false; + selectionArea.reset(); + } + } + } else { + if ((ourMetaState & OUR_CTRL_ON) != 0) { + sendEscape(); + ourMetaState &= ~OUR_CTRL_ON; + } else + metaPress(OUR_CTRL_ON); + } + bridge.redraw(); + return true; + } + + int derivedMetaState = event.getMetaState(); + if ((ourMetaState & OUR_SHIFT_MASK) != 0) + derivedMetaState |= KeyEvent.META_SHIFT_ON; + if ((ourMetaState & OUR_ALT_MASK) != 0) + derivedMetaState |= KeyEvent.META_ALT_ON; + if ((ourMetaState & OUR_CTRL_MASK) != 0) + derivedMetaState |= HC_META_CTRL_ON; + + if ((ourMetaState & OUR_TRANSIENT) != 0) { + ourMetaState &= ~OUR_TRANSIENT; + bridge.redraw(); + } + + // Test for modified numbers becoming function keys + if (shiftedNumbersAreFKeys && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) { + if (sendFunctionKey(keyCode)) + return true; + } + if (controlNumbersAreFKeys && (derivedMetaState & HC_META_CTRL_ON) != 0) { + if (sendFunctionKey(keyCode)) + return true; + } + + // Ask the system to use the keymap to give us the unicode character for this key, + // with our derived modifier state applied. + int uchar = event.getUnicodeChar(derivedMetaState & ~HC_META_CTRL_MASK); + int ucharWithoutAlt = event.getUnicodeChar( + derivedMetaState & ~(HC_META_ALT_MASK | HC_META_CTRL_MASK)); + if (uchar != ucharWithoutAlt) { + // The alt key was used to modify the character returned; therefore, drop the alt + // modifier from the state so we don't end up sending alt+key. + derivedMetaState &= ~HC_META_ALT_MASK; + } + + // Remove shift from the modifier state as it has already been used by getUnicodeChar. + derivedMetaState &= ~KeyEvent.META_SHIFT_ON; + + if ((uchar & KeyCharacterMap.COMBINING_ACCENT) != 0) { + mDeadKey = uchar & KeyCharacterMap.COMBINING_ACCENT_MASK; + return true; + } + + if (mDeadKey != 0) { + uchar = KeyCharacterMap.getDeadChar(mDeadKey, keyCode); + mDeadKey = 0; + } + + // If we have a defined non-control character + if (uchar >= 0x20) { + if ((derivedMetaState & HC_META_CTRL_ON) != 0) + uchar = keyAsControl(uchar); + if ((derivedMetaState & KeyEvent.META_ALT_ON) != 0) + sendEscape(); + if (uchar < 0x80) + bridge.transport.write(uchar); + else + // TODO write encoding routine that doesn't allocate each time + bridge.transport.write(new String(Character.toChars(uchar)) + .getBytes(encoding)); + return true; + } + + // look for special chars + switch(keyCode) { + case KEYCODE_ESCAPE: + sendEscape(); + return true; + case KeyEvent.KEYCODE_TAB: + bridge.transport.write(0x09); + return true; + case KeyEvent.KEYCODE_CAMERA: + + // check to see which shortcut the camera button triggers + String camera = manager.prefs.getString( + PreferenceConstants.CAMERA, + PreferenceConstants.CAMERA_CTRLA_SPACE); + if(PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) { + bridge.transport.write(0x01); + bridge.transport.write(' '); + } else if(PreferenceConstants.CAMERA_CTRLA.equals(camera)) { + bridge.transport.write(0x01); + } else if(PreferenceConstants.CAMERA_ESC.equals(camera)) { + ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0); + } else if(PreferenceConstants.CAMERA_ESC_A.equals(camera)) { + ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0); + bridge.transport.write('a'); + } + + break; + + case KeyEvent.KEYCODE_DEL: + ((vt320) buffer).keyPressed(vt320.KEY_BACK_SPACE, ' ', + getStateForBuffer()); + return true; + case KeyEvent.KEYCODE_ENTER: + ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', 0); + return true; + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (selectingForCopy) { + selectionArea.decrementColumn(); + bridge.redraw(); + } else { + ((vt320) buffer).keyPressed(vt320.KEY_LEFT, ' ', + getStateForBuffer()); + bridge.tryKeyVibrate(); + } + return true; + + case KeyEvent.KEYCODE_DPAD_UP: + if (selectingForCopy) { + selectionArea.decrementRow(); + bridge.redraw(); + } else { + ((vt320) buffer).keyPressed(vt320.KEY_UP, ' ', + getStateForBuffer()); + bridge.tryKeyVibrate(); + } + return true; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (selectingForCopy) { + selectionArea.incrementRow(); + bridge.redraw(); + } else { + ((vt320) buffer).keyPressed(vt320.KEY_DOWN, ' ', + getStateForBuffer()); + bridge.tryKeyVibrate(); + } + return true; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (selectingForCopy) { + selectionArea.incrementColumn(); + bridge.redraw(); + } else { + ((vt320) buffer).keyPressed(vt320.KEY_RIGHT, ' ', + getStateForBuffer()); + bridge.tryKeyVibrate(); + } + return true; + } + + } catch (IOException e) { + Log.e(TAG, "Problem while trying to handle an onKey() event", e); + try { + bridge.transport.flush(); + } catch (IOException ioe) { + Log.d(TAG, "Our transport was closed, dispatching disconnect event"); + bridge.dispatchDisconnect(false); + } + } catch (NullPointerException npe) { + Log.d(TAG, "Input before connection established ignored."); + return true; + } + + return false; + } + + public int keyAsControl(int key) { + // Support CTRL-a through CTRL-z + if (key >= 0x61 && key <= 0x7A) + key -= 0x60; + // Support CTRL-A through CTRL-_ + else if (key >= 0x41 && key <= 0x5F) + key -= 0x40; + // CTRL-space sends NULL + else if (key == 0x20) + key = 0x00; + // CTRL-? sends DEL + else if (key == 0x3F) + key = 0x7F; + return key; + } + + public void sendEscape() { + ((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0); + } + + /** + * @param key + * @return successful + */ + private boolean sendFunctionKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_1: + ((vt320) buffer).keyPressed(vt320.KEY_F1, ' ', 0); + return true; + case KeyEvent.KEYCODE_2: + ((vt320) buffer).keyPressed(vt320.KEY_F2, ' ', 0); + return true; + case KeyEvent.KEYCODE_3: + ((vt320) buffer).keyPressed(vt320.KEY_F3, ' ', 0); + return true; + case KeyEvent.KEYCODE_4: + ((vt320) buffer).keyPressed(vt320.KEY_F4, ' ', 0); + return true; + case KeyEvent.KEYCODE_5: + ((vt320) buffer).keyPressed(vt320.KEY_F5, ' ', 0); + return true; + case KeyEvent.KEYCODE_6: + ((vt320) buffer).keyPressed(vt320.KEY_F6, ' ', 0); + return true; + case KeyEvent.KEYCODE_7: + ((vt320) buffer).keyPressed(vt320.KEY_F7, ' ', 0); + return true; + case KeyEvent.KEYCODE_8: + ((vt320) buffer).keyPressed(vt320.KEY_F8, ' ', 0); + return true; + case KeyEvent.KEYCODE_9: + ((vt320) buffer).keyPressed(vt320.KEY_F9, ' ', 0); + return true; + case KeyEvent.KEYCODE_0: + ((vt320) buffer).keyPressed(vt320.KEY_F10, ' ', 0); + return true; + default: + return false; + } + } + + /** + * Handle meta key presses where the key can be locked on. + *

+ * 1st press: next key to have meta state
+ * 2nd press: meta state is locked on
+ * 3rd press: disable meta state + * + * @param code + */ + public void metaPress(int code) { + if ((ourMetaState & (code << 1)) != 0) { + ourMetaState &= ~(code << 1); + } else if ((ourMetaState & code) != 0) { + ourMetaState &= ~code; + ourMetaState |= code << 1; + } else + ourMetaState |= code; + bridge.redraw(); + } + + public void setTerminalKeyMode(String keymode) { + this.keymode = keymode; + } + + private int getStateForBuffer() { + int bufferState = 0; + + if ((ourMetaState & OUR_CTRL_MASK) != 0) + bufferState |= vt320.KEY_CONTROL; + if ((ourMetaState & OUR_SHIFT_MASK) != 0) + bufferState |= vt320.KEY_SHIFT; + if ((ourMetaState & OUR_ALT_MASK) != 0) + bufferState |= vt320.KEY_ALT; + + return bufferState; + } + + public int getMetaState() { + return ourMetaState; + } + + public int getDeadKey() { + return mDeadKey; + } + + public void setClipboardManager(ClipboardManager clipboard) { + this.clipboard = clipboard; + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (PreferenceConstants.KEYMODE.equals(key) || + PreferenceConstants.SHIFT_FKEYS.equals(key) || + PreferenceConstants.CTRL_FKEYS.equals(key) || + PreferenceConstants.VOLUME_FONT.equals(key)) { + updatePrefs(); + } + } + + private void updatePrefs() { + keymode = prefs.getString(PreferenceConstants.KEYMODE, PreferenceConstants.KEYMODE_RIGHT); + shiftedNumbersAreFKeysOnHardKeyboard = + prefs.getBoolean(PreferenceConstants.SHIFT_FKEYS, false); + controlNumbersAreFKeysOnSoftKeyboard = + prefs.getBoolean(PreferenceConstants.CTRL_FKEYS, false); + volumeKeysChangeFontSize = prefs.getBoolean(PreferenceConstants.VOLUME_FONT, true); + } + + public void setCharset(String encoding) { + this.encoding = encoding; + } +} diff --git a/app/src/main/java/org/connectbot/service/TerminalManager.java b/app/src/main/java/org/connectbot/service/TerminalManager.java new file mode 100644 index 0000000..369d79a --- /dev/null +++ b/app/src/main/java/org/connectbot/service/TerminalManager.java @@ -0,0 +1,714 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.service; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Timer; +import java.util.TimerTask; + +import org.connectbot.R; +import org.connectbot.bean.HostBean; +import org.connectbot.bean.PubkeyBean; +import org.connectbot.transport.TransportFactory; +import org.connectbot.util.HostDatabase; +import org.connectbot.util.PreferenceConstants; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.AssetFileDescriptor; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Vibrator; +import android.preference.PreferenceManager; +import android.util.Log; + +/** + * Manager for SSH connections that runs as a service. This service holds a list + * of currently connected SSH bridges that are ready for connection up to a GUI + * if needed. + * + * @author jsharkey + */ +public class TerminalManager extends Service implements BridgeDisconnectedListener, OnSharedPreferenceChangeListener { + public final static String TAG = "ConnectBot.TerminalManager"; + + public List bridges = new LinkedList(); + public Map> mHostBridgeMap = + new HashMap>(); + public Map> mNicknameBridgeMap = + new HashMap>(); + + public TerminalBridge defaultBridge = null; + + public List disconnected = new LinkedList(); + + public Handler disconnectHandler = null; + + public Map loadedKeypairs = new HashMap(); + + public Resources res; + + public HostDatabase hostdb; + public PubkeyDatabase pubkeydb; + + protected SharedPreferences prefs; + + final private IBinder binder = new TerminalBinder(); + + private ConnectivityReceiver connectivityManager; + + private MediaPlayer mediaPlayer; + + private Timer pubkeyTimer; + + private Timer idleTimer; + private final long IDLE_TIMEOUT = 300000; // 5 minutes + + private Vibrator vibrator; + private volatile boolean wantKeyVibration; + public static final long VIBRATE_DURATION = 30; + + private boolean wantBellVibration; + + private boolean resizeAllowed = true; + + private boolean savingKeys; + + protected List> mPendingReconnect + = new LinkedList>(); + + public boolean hardKeyboardHidden; + + @Override + public void onCreate() { + Log.i(TAG, "Starting service"); + + prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + + res = getResources(); + + pubkeyTimer = new Timer("pubkeyTimer", true); + + hostdb = new HostDatabase(this); + pubkeydb = new PubkeyDatabase(this); + + // load all marked pubkeys into memory + updateSavingKeys(); + List pubkeys = pubkeydb.getAllStartPubkeys(); + + for (PubkeyBean pubkey : pubkeys) { + try { + PrivateKey privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), pubkey.getType()); + PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); + KeyPair pair = new KeyPair(pubKey, privKey); + + addKey(pubkey, pair); + } catch (Exception e) { + Log.d(TAG, String.format("Problem adding key '%s' to in-memory cache", pubkey.getNickname()), e); + } + } + + vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + wantKeyVibration = prefs.getBoolean(PreferenceConstants.BUMPY_ARROWS, true); + + wantBellVibration = prefs.getBoolean(PreferenceConstants.BELL_VIBRATE, true); + enableMediaPlayer(); + + hardKeyboardHidden = (res.getConfiguration().hardKeyboardHidden == + Configuration.HARDKEYBOARDHIDDEN_YES); + + final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true); + + connectivityManager = new ConnectivityReceiver(this, lockingWifi); + + } + + private void updateSavingKeys() { + savingKeys = prefs.getBoolean(PreferenceConstants.MEMKEYS, true); + } + + @Override + public void onDestroy() { + Log.i(TAG, "Destroying service"); + + disconnectAll(true); + + if(hostdb != null) { + hostdb.close(); + hostdb = null; + } + + if(pubkeydb != null) { + pubkeydb.close(); + pubkeydb = null; + } + + synchronized (this) { + if (idleTimer != null) + idleTimer.cancel(); + if (pubkeyTimer != null) + pubkeyTimer.cancel(); + } + + connectivityManager.cleanup(); + + ConnectionNotifier.getInstance().hideRunningNotification(this); + + disableMediaPlayer(); + } + + /** + * Disconnect all currently connected bridges. + */ + private void disconnectAll(final boolean immediate) { + TerminalBridge[] tmpBridges = null; + + synchronized (bridges) { + if (bridges.size() > 0) { + tmpBridges = bridges.toArray(new TerminalBridge[bridges.size()]); + } + } + + if (tmpBridges != null) { + // disconnect and dispose of any existing bridges + for (int i = 0; i < tmpBridges.length; i++) + tmpBridges[i].dispatchDisconnect(immediate); + } + } + + /** + * Open a new SSH session using the given parameters. + */ + private TerminalBridge openConnection(HostBean host) throws IllegalArgumentException, IOException { + // throw exception if terminal already open + if (getConnectedBridge(host) != null) { + throw new IllegalArgumentException("Connection already open for that nickname"); + } + + TerminalBridge bridge = new TerminalBridge(this, host); + bridge.setOnDisconnectedListener(this); + bridge.startConnection(); + + synchronized (bridges) { + bridges.add(bridge); + WeakReference wr = new WeakReference(bridge); + mHostBridgeMap.put(bridge.host, wr); + mNicknameBridgeMap.put(bridge.host.getNickname(), wr); + } + + synchronized (disconnected) { + disconnected.remove(bridge.host); + } + + if (bridge.isUsingNetwork()) { + connectivityManager.incRef(); + } + + if (prefs.getBoolean(PreferenceConstants.CONNECTION_PERSIST, true)) { + ConnectionNotifier.getInstance().showRunningNotification(this); + } + + // also update database with new connected time + touchHost(host); + + return bridge; + } + + public String getEmulation() { + return prefs.getString(PreferenceConstants.EMULATION, "screen"); + } + + public int getScrollback() { + int scrollback = 140; + try { + scrollback = Integer.parseInt(prefs.getString(PreferenceConstants.SCROLLBACK, "140")); + } catch(Exception e) { + } + return scrollback; + } + + /** + * Open a new connection by reading parameters from the given URI. Follows + * format specified by an individual transport. + */ + public TerminalBridge openConnection(Uri uri) throws Exception { + HostBean host = TransportFactory.findHost(hostdb, uri); + + if (host == null) + host = TransportFactory.getTransport(uri.getScheme()).createHost(uri); + + return openConnection(host); + } + + /** + * Update the last-connected value for the given nickname by passing through + * to {@link HostDatabase}. + */ + private void touchHost(HostBean host) { + hostdb.touchHost(host); + } + + /** + * Find a connected {@link TerminalBridge} with the given HostBean. + * + * @param host the HostBean to search for + * @return TerminalBridge that uses the HostBean + */ + public TerminalBridge getConnectedBridge(HostBean host) { + WeakReference wr = mHostBridgeMap.get(host); + if (wr != null) { + return wr.get(); + } else { + return null; + } + } + + /** + * Find a connected {@link TerminalBridge} using its nickname. + * + * @param nickname + * @return TerminalBridge that matches nickname + */ + public TerminalBridge getConnectedBridge(final String nickname) { + if (nickname == null) { + return null; + } + WeakReference wr = mNicknameBridgeMap.get(nickname); + if (wr != null) { + return wr.get(); + } else { + return null; + } + } + + /** + * Called by child bridge when somehow it's been disconnected. + */ + public void onDisconnected(TerminalBridge bridge) { + boolean shouldHideRunningNotification = false; + + synchronized (bridges) { + // remove this bridge from our list + bridges.remove(bridge); + + mHostBridgeMap.remove(bridge.host); + mNicknameBridgeMap.remove(bridge.host.getNickname()); + + if (bridge.isUsingNetwork()) { + connectivityManager.decRef(); + } + + if (bridges.size() == 0 && + mPendingReconnect.size() == 0) { + shouldHideRunningNotification = true; + } + } + + synchronized (disconnected) { + disconnected.add(bridge.host); + } + + if (shouldHideRunningNotification) { + ConnectionNotifier.getInstance().hideRunningNotification(this); + } + + // pass notification back up to gui + if (disconnectHandler != null) + Message.obtain(disconnectHandler, -1, bridge).sendToTarget(); + } + + public boolean isKeyLoaded(String nickname) { + return loadedKeypairs.containsKey(nickname); + } + + public void addKey(PubkeyBean pubkey, KeyPair pair) { + addKey(pubkey, pair, false); + } + + public void addKey(PubkeyBean pubkey, KeyPair pair, boolean force) { + if (!savingKeys && !force) + return; + + removeKey(pubkey.getNickname()); + + byte[] sshPubKey = PubkeyUtils.extractOpenSSHPublic(pair); + + KeyHolder keyHolder = new KeyHolder(); + keyHolder.bean = pubkey; + keyHolder.pair = pair; + keyHolder.openSSHPubkey = sshPubKey; + + loadedKeypairs.put(pubkey.getNickname(), keyHolder); + + if (pubkey.getLifetime() > 0) { + final String nickname = pubkey.getNickname(); + pubkeyTimer.schedule(new TimerTask() { + @Override + public void run() { + Log.d(TAG, "Unloading from memory key: " + nickname); + removeKey(nickname); + } + }, pubkey.getLifetime() * 1000); + } + + Log.d(TAG, String.format("Added key '%s' to in-memory cache", pubkey.getNickname())); + } + + public boolean removeKey(String nickname) { + Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname)); + return loadedKeypairs.remove(nickname) != null; + } + + public boolean removeKey(byte[] publicKey) { + String nickname = null; + for (Entry entry : loadedKeypairs.entrySet()) { + if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) { + nickname = entry.getKey(); + break; + } + } + + if (nickname != null) { + Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname)); + return removeKey(nickname); + } else + return false; + } + + public KeyPair getKey(String nickname) { + if (loadedKeypairs.containsKey(nickname)) { + KeyHolder keyHolder = loadedKeypairs.get(nickname); + return keyHolder.pair; + } else + return null; + } + + public KeyPair getKey(byte[] publicKey) { + for (KeyHolder keyHolder : loadedKeypairs.values()) { + if (Arrays.equals(keyHolder.openSSHPubkey, publicKey)) + return keyHolder.pair; + } + return null; + } + + public String getKeyNickname(byte[] publicKey) { + for (Entry entry : loadedKeypairs.entrySet()) { + if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) + return entry.getKey(); + } + return null; + } + + private void stopWithDelay() { + // TODO add in a way to check whether keys loaded are encrypted and only + // set timer when we have an encrypted key loaded + + if (loadedKeypairs.size() > 0) { + synchronized (this) { + if (idleTimer == null) + idleTimer = new Timer("idleTimer", true); + + idleTimer.schedule(new IdleTask(), IDLE_TIMEOUT); + } + } else { + Log.d(TAG, "Stopping service immediately"); + stopSelf(); + } + } + + protected void stopNow() { + if (bridges.size() == 0) { + stopSelf(); + } + } + + private synchronized void stopIdleTimer() { + if (idleTimer != null) { + idleTimer.cancel(); + idleTimer = null; + } + } + + public class TerminalBinder extends Binder { + public TerminalManager getService() { + return TerminalManager.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + Log.i(TAG, "Someone bound to TerminalManager"); + + setResizeAllowed(true); + + stopIdleTimer(); + + // Make sure we stay running to maintain the bridges + startService(new Intent(this, TerminalManager.class)); + + return binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + /* + * We want this service to continue running until it is explicitly + * stopped, so return sticky. + */ + return START_STICKY; + } + + @Override + public void onRebind(Intent intent) { + super.onRebind(intent); + + setResizeAllowed(true); + + Log.i(TAG, "Someone rebound to TerminalManager"); + + stopIdleTimer(); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.i(TAG, "Someone unbound from TerminalManager"); + + setResizeAllowed(true); + + if (bridges.size() == 0) { + stopWithDelay(); + } + + return true; + } + + private class IdleTask extends TimerTask { + /* (non-Javadoc) + * @see java.util.TimerTask#run() + */ + @Override + public void run() { + Log.d(TAG, String.format("Stopping service after timeout of ~%d seconds", IDLE_TIMEOUT / 1000)); + TerminalManager.this.stopNow(); + } + } + + public void tryKeyVibrate() { + if (wantKeyVibration) + vibrate(); + } + + private void vibrate() { + if (vibrator != null) + vibrator.vibrate(VIBRATE_DURATION); + } + + private void enableMediaPlayer() { + mediaPlayer = new MediaPlayer(); + + float volume = prefs.getFloat(PreferenceConstants.BELL_VOLUME, + PreferenceConstants.DEFAULT_BELL_VOLUME); + + mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); + mediaPlayer.setOnCompletionListener(new BeepListener()); + + AssetFileDescriptor file = res.openRawResourceFd(R.raw.bell); + try { + mediaPlayer.setDataSource(file.getFileDescriptor(), file + .getStartOffset(), file.getLength()); + file.close(); + mediaPlayer.setVolume(volume, volume); + mediaPlayer.prepare(); + } catch (IOException e) { + Log.e(TAG, "Error setting up bell media player", e); + } + } + + private void disableMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } + + public void playBeep() { + if (mediaPlayer != null) + mediaPlayer.start(); + + if (wantBellVibration) + vibrate(); + } + + private static class BeepListener implements OnCompletionListener { + public void onCompletion(MediaPlayer mp) { + mp.seekTo(0); + } + } + + /** + * Send system notification to user for a certain host. When user selects + * the notification, it will bring them directly to the ConsoleActivity + * displaying the host. + * + * @param host + */ + public void sendActivityNotification(HostBean host) { + if (!prefs.getBoolean(PreferenceConstants.BELL_NOTIFICATION, false)) + return; + + ConnectionNotifier.getInstance().showActivityNotification(this, host); + } + + /* (non-Javadoc) + * @see android.content.SharedPreferences.OnSharedPreferenceChangeListener#onSharedPreferenceChanged(android.content.SharedPreferences, java.lang.String) + */ + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (PreferenceConstants.BELL.equals(key)) { + boolean wantAudible = sharedPreferences.getBoolean( + PreferenceConstants.BELL, true); + if (wantAudible && mediaPlayer == null) + enableMediaPlayer(); + else if (!wantAudible && mediaPlayer != null) + disableMediaPlayer(); + } else if (PreferenceConstants.BELL_VOLUME.equals(key)) { + if (mediaPlayer != null) { + float volume = sharedPreferences.getFloat( + PreferenceConstants.BELL_VOLUME, + PreferenceConstants.DEFAULT_BELL_VOLUME); + mediaPlayer.setVolume(volume, volume); + } + } else if (PreferenceConstants.BELL_VIBRATE.equals(key)) { + wantBellVibration = sharedPreferences.getBoolean( + PreferenceConstants.BELL_VIBRATE, true); + } else if (PreferenceConstants.BUMPY_ARROWS.equals(key)) { + wantKeyVibration = sharedPreferences.getBoolean( + PreferenceConstants.BUMPY_ARROWS, true); + } else if (PreferenceConstants.WIFI_LOCK.equals(key)) { + final boolean lockingWifi = prefs.getBoolean(PreferenceConstants.WIFI_LOCK, true); + connectivityManager.setWantWifiLock(lockingWifi); + } else if (PreferenceConstants.MEMKEYS.equals(key)) { + updateSavingKeys(); + } + } + + /** + * Allow {@link TerminalBridge} to resize when the parent has changed. + * @param resizeAllowed + */ + public void setResizeAllowed(boolean resizeAllowed) { + this.resizeAllowed = resizeAllowed; + } + + public boolean isResizeAllowed() { + return resizeAllowed; + } + + public static class KeyHolder { + public PubkeyBean bean; + public KeyPair pair; + public byte[] openSSHPubkey; + } + + /** + * Called when connectivity to the network is lost and it doesn't appear + * we'll be getting a different connection any time soon. + */ + public void onConnectivityLost() { + final Thread t = new Thread() { + @Override + public void run() { + disconnectAll(false); + } + }; + t.setName("Disconnector"); + t.start(); + } + + /** + * Called when connectivity to the network is restored. + */ + public void onConnectivityRestored() { + final Thread t = new Thread() { + @Override + public void run() { + reconnectPending(); + } + }; + t.setName("Reconnector"); + t.start(); + } + + /** + * Insert request into reconnect queue to be executed either immediately + * or later when connectivity is restored depending on whether we're + * currently connected. + * + * @param bridge the TerminalBridge to reconnect when possible + */ + public void requestReconnect(TerminalBridge bridge) { + synchronized (mPendingReconnect) { + mPendingReconnect.add(new WeakReference(bridge)); + if (!bridge.isUsingNetwork() || + connectivityManager.isConnected()) { + reconnectPending(); + } + } + } + + /** + * Reconnect all bridges that were pending a reconnect when connectivity + * was lost. + */ + private void reconnectPending() { + synchronized (mPendingReconnect) { + for (WeakReference ref : mPendingReconnect) { + TerminalBridge bridge = ref.get(); + if (bridge == null) { + continue; + } + bridge.startConnection(); + } + mPendingReconnect.clear(); + } + } +} diff --git a/app/src/main/java/org/connectbot/transport/AbsTransport.java b/app/src/main/java/org/connectbot/transport/AbsTransport.java new file mode 100644 index 0000000..18397ea --- /dev/null +++ b/app/src/main/java/org/connectbot/transport/AbsTransport.java @@ -0,0 +1,254 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.transport; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.connectbot.bean.HostBean; +import org.connectbot.bean.PortForwardBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; + +import android.content.Context; +import android.net.Uri; + +/** + * @author Kenny Root + * + */ +public abstract class AbsTransport { + HostBean host; + TerminalBridge bridge; + TerminalManager manager; + + String emulation; + + public AbsTransport() {} + + public AbsTransport(HostBean host, TerminalBridge bridge, TerminalManager manager) { + this.host = host; + this.bridge = bridge; + this.manager = manager; + } + + /** + * @return protocol part of the URI + */ + public static String getProtocolName() { + return "unknown"; + } + + /** + * Encode the current transport into a URI that can be passed via intent calls. + * @return URI to host + */ + public static Uri getUri(String input) { + return null; + } + + /** + * Causes transport to connect to the target host. After connecting but before a + * session is started, must call back to {@link TerminalBridge#onConnected()}. + * After that call a session may be opened. + */ + public abstract void connect(); + + /** + * Reads from the transport. Transport must support reading into a the byte array + * buffer at the start of offset and a maximum of + * length bytes. If the remote host disconnects, throw an + * {@link IOException}. + * @param buffer byte buffer to store read bytes into + * @param offset where to start writing in the buffer + * @param length maximum number of bytes to read + * @return number of bytes read + * @throws IOException when remote host disconnects + */ + public abstract int read(byte[] buffer, int offset, int length) throws IOException; + + /** + * Writes to the transport. If the host is not yet connected, simply return without + * doing anything. An {@link IOException} should be thrown if there is an error after + * connection. + * @param buffer bytes to write to transport + * @throws IOException when there is a problem writing after connection + */ + public abstract void write(byte[] buffer) throws IOException; + + /** + * Writes to the transport. See {@link #write(byte[])} for behavior details. + * @param c character to write to the transport + * @throws IOException when there is a problem writing after connection + */ + public abstract void write(int c) throws IOException; + + /** + * Flushes the write commands to the transport. + * @throws IOException when there is a problem writing after connection + */ + public abstract void flush() throws IOException; + + /** + * Closes the connection to the terminal. Note that the resulting failure to read + * should call {@link TerminalBridge#dispatchDisconnect(boolean)}. + */ + public abstract void close(); + + /** + * Tells the transport what dimensions the display is currently + * @param columns columns of text + * @param rows rows of text + * @param width width in pixels + * @param height height in pixels + */ + public abstract void setDimensions(int columns, int rows, int width, int height); + + public void setOptions(Map options) { + // do nothing + } + + public Map getOptions() { + return null; + } + + public void setCompression(boolean compression) { + // do nothing + } + + public void setUseAuthAgent(String useAuthAgent) { + // do nothing + } + + public void setEmulation(String emulation) { + this.emulation = emulation; + } + + public String getEmulation() { + return emulation; + } + + public void setHost(HostBean host) { + this.host = host; + } + + public void setBridge(TerminalBridge bridge) { + this.bridge = bridge; + } + + public void setManager(TerminalManager manager) { + this.manager = manager; + } + + /** + * Whether or not this transport type can forward ports. + * @return true on ability to forward ports + */ + public boolean canForwardPorts() { + return false; + } + + /** + * Adds the {@link PortForwardBean} to the list. + * @param portForward the port forward bean to add + * @return true on successful addition + */ + public boolean addPortForward(PortForwardBean portForward) { + return false; + } + + /** + * Enables a port forward member. After calling this method, the port forward should + * be operational iff it could be enabled by the transport. + * @param portForward member of our current port forwards list to enable + * @return true on successful port forward setup + */ + public boolean enablePortForward(PortForwardBean portForward) { + return false; + } + + /** + * Disables a port forward member. After calling this method, the port forward should + * be non-functioning iff it could be disabled by the transport. + * @param portForward member of our current port forwards list to enable + * @return true on successful port forward tear-down + */ + public boolean disablePortForward(PortForwardBean portForward) { + return false; + } + + /** + * Removes the {@link PortForwardBean} from the available port forwards. + * @param portForward the port forward bean to remove + * @return true on successful removal + */ + public boolean removePortForward(PortForwardBean portForward) { + return false; + } + + /** + * Gets a list of the {@link PortForwardBean} currently used by this transport. + * @return the list of port forwards + */ + public List getPortForwards() { + return null; + } + + public abstract boolean isConnected(); + public abstract boolean isSessionOpen(); + + /** + * @return int default port for protocol + */ + public abstract int getDefaultPort(); + + /** + * @param username + * @param hostname + * @param port + * @return + */ + public abstract String getDefaultNickname(String username, String hostname, int port); + + /** + * @param uri + * @param selectionKeys + * @param selectionValues + */ + public abstract void getSelectionArgs(Uri uri, Map selection); + + /** + * @param uri + * @return + */ + public abstract HostBean createHost(Uri uri); + + /** + * @param context context containing the correct resources + * @return string that hints at the format for connection + */ + public static String getFormatHint(Context context) { + return "???"; + } + + /** + * @return + */ + public abstract boolean usesNetwork(); +} diff --git a/app/src/main/java/org/connectbot/transport/Local.java b/app/src/main/java/org/connectbot/transport/Local.java new file mode 100644 index 0000000..5ace1b0 --- /dev/null +++ b/app/src/main/java/org/connectbot/transport/Local.java @@ -0,0 +1,219 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.transport; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; + +import org.connectbot.R; +import org.connectbot.bean.HostBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.HostDatabase; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.google.ase.Exec; + +/** + * @author Kenny Root + * + */ +public class Local extends AbsTransport { + private static final String TAG = "ConnectBot.Local"; + private static final String PROTOCOL = "local"; + + private static final String DEFAULT_URI = "local:#Local"; + + private FileDescriptor shellFd; + + private FileInputStream is; + private FileOutputStream os; + + /** + * + */ + public Local() { + } + + /** + * @param host + * @param bridge + * @param manager + */ + public Local(HostBean host, TerminalBridge bridge, TerminalManager manager) { + super(host, bridge, manager); + } + + public static String getProtocolName() { + return PROTOCOL; + } + + @Override + public void close() { + try { + if (os != null) { + os.close(); + os = null; + } + if (is != null) { + is.close(); + is = null; + } + } catch (IOException e) { + Log.e(TAG, "Couldn't close shell", e); + } + } + + @Override + public void connect() { + int[] pids = new int[1]; + + try { + shellFd = Exec.createSubprocess("/system/bin/sh", "-", null, pids); + } catch (Exception e) { + bridge.outputLine(manager.res.getString(R.string.local_shell_unavailable)); + Log.e(TAG, "Cannot start local shell", e); + return; + } + + final int shellPid = pids[0]; + Runnable exitWatcher = new Runnable() { + public void run() { + Exec.waitFor(shellPid); + + bridge.dispatchDisconnect(false); + } + }; + + Thread exitWatcherThread = new Thread(exitWatcher); + exitWatcherThread.setName("LocalExitWatcher"); + exitWatcherThread.setDaemon(true); + exitWatcherThread.start(); + + is = new FileInputStream(shellFd); + os = new FileOutputStream(shellFd); + + bridge.onConnected(); + } + + @Override + public void flush() throws IOException { + os.flush(); + } + + @Override + public String getDefaultNickname(String username, String hostname, int port) { + return DEFAULT_URI; + } + + @Override + public int getDefaultPort() { + return 0; + } + + @Override + public boolean isConnected() { + return is != null && os != null; + } + + @Override + public boolean isSessionOpen() { + return is != null && os != null; + } + + @Override + public int read(byte[] buffer, int start, int len) throws IOException { + if (is == null) { + bridge.dispatchDisconnect(false); + throw new IOException("session closed"); + } + return is.read(buffer, start, len); + } + + @Override + public void setDimensions(int columns, int rows, int width, int height) { + try { + Exec.setPtyWindowSize(shellFd, rows, columns, width, height); + } catch (Exception e) { + Log.e(TAG, "Couldn't resize pty", e); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + if (os != null) + os.write(buffer); + } + + @Override + public void write(int c) throws IOException { + if (os != null) + os.write(c); + } + + public static Uri getUri(String input) { + Uri uri = Uri.parse(DEFAULT_URI); + + if (input != null && input.length() > 0) { + uri = uri.buildUpon().fragment(input).build(); + } + + return uri; + } + + @Override + public HostBean createHost(Uri uri) { + HostBean host = new HostBean(); + + host.setProtocol(PROTOCOL); + + String nickname = uri.getFragment(); + if (nickname == null || nickname.length() == 0) { + host.setNickname(getDefaultNickname(host.getUsername(), + host.getHostname(), host.getPort())); + } else { + host.setNickname(uri.getFragment()); + } + + return host; + } + + @Override + public void getSelectionArgs(Uri uri, Map selection) { + selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL); + selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment()); + } + + public static String getFormatHint(Context context) { + return context.getString(R.string.hostpref_nickname_title); + } + + /* (non-Javadoc) + * @see org.connectbot.transport.AbsTransport#usesNetwork() + */ + @Override + public boolean usesNetwork() { + return false; + } +} diff --git a/app/src/main/java/org/connectbot/transport/SSH.java b/app/src/main/java/org/connectbot/transport/SSH.java new file mode 100644 index 0000000..6ef9745 --- /dev/null +++ b/app/src/main/java/org/connectbot/transport/SSH.java @@ -0,0 +1,958 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.connectbot.R; +import org.connectbot.bean.HostBean; +import org.connectbot.bean.PortForwardBean; +import org.connectbot.bean.PubkeyBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.service.TerminalManager.KeyHolder; +import org.connectbot.util.HostDatabase; +import org.connectbot.util.PubkeyDatabase; +import org.connectbot.util.PubkeyUtils; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.ChannelCondition; +import com.trilead.ssh2.Connection; +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.DynamicPortForwarder; +import com.trilead.ssh2.InteractiveCallback; +import com.trilead.ssh2.KnownHosts; +import com.trilead.ssh2.LocalPortForwarder; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.Session; +import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + +/** + * @author Kenny Root + * + */ +public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback, AuthAgentCallback { + public SSH() { + super(); + } + + /** + * @param bridge + * @param db + */ + public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) { + super(host, bridge, manager); + } + + private static final String PROTOCOL = "ssh"; + private static final String TAG = "ConnectBot.SSH"; + private static final int DEFAULT_PORT = 22; + + private static final String AUTH_PUBLICKEY = "publickey", + AUTH_PASSWORD = "password", + AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive"; + + private final static int AUTH_TRIES = 20; + + static final Pattern hostmask; + static { + hostmask = Pattern.compile("^(.+)@([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE); + } + + private boolean compression = false; + private volatile boolean authenticated = false; + private volatile boolean connected = false; + private volatile boolean sessionOpen = false; + + private boolean pubkeysExhausted = false; + private boolean interactiveCanContinue = true; + + private Connection connection; + private Session session; + private ConnectionInfo connectionInfo; + + private OutputStream stdin; + private InputStream stdout; + private InputStream stderr; + + private static final int conditions = ChannelCondition.STDOUT_DATA + | ChannelCondition.STDERR_DATA + | ChannelCondition.CLOSED + | ChannelCondition.EOF; + + private List portForwards = new LinkedList(); + + private int columns; + private int rows; + + private int width; + private int height; + + private String useAuthAgent = HostDatabase.AUTHAGENT_NO; + private String agentLockPassphrase; + + public class HostKeyVerifier implements ServerHostKeyVerifier { + public boolean verifyServerHostKey(String hostname, int port, + String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { + + // read in all known hosts from hostdb + KnownHosts hosts = manager.hostdb.getKnownHosts(); + Boolean result; + + String matchName = String.format(Locale.US, "%s:%d", hostname, port); + + String fingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey); + + String algorithmName; + if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + algorithmName = "RSA"; + else if ("ssh-dss".equals(serverHostKeyAlgorithm)) + algorithmName = "DSA"; + else if (serverHostKeyAlgorithm.startsWith("ecdsa-")) + algorithmName = "EC"; + else + algorithmName = serverHostKeyAlgorithm; + + switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) { + case KnownHosts.HOSTKEY_IS_OK: + bridge.outputLine(manager.res.getString(R.string.terminal_sucess, algorithmName, fingerprint)); + return true; + + case KnownHosts.HOSTKEY_IS_NEW: + // prompt user + bridge.outputLine(manager.res.getString(R.string.host_authenticity_warning, hostname)); + bridge.outputLine(manager.res.getString(R.string.host_fingerprint, algorithmName, fingerprint)); + + result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting)); + if(result == null) return false; + if(result.booleanValue()) { + // save this key in known database + manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); + } + return result.booleanValue(); + + case KnownHosts.HOSTKEY_HAS_CHANGED: + String header = String.format("@ %s @", + manager.res.getString(R.string.host_verification_failure_warning_header)); + + char[] atsigns = new char[header.length()]; + Arrays.fill(atsigns, '@'); + String border = new String(atsigns); + + bridge.outputLine(border); + bridge.outputLine(manager.res.getString(R.string.host_verification_failure_warning)); + bridge.outputLine(border); + + bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint), + algorithmName, fingerprint)); + + // Users have no way to delete keys, so we'll prompt them for now. + result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting)); + if(result == null) return false; + if(result.booleanValue()) { + // save this key in known database + manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); + } + return result.booleanValue(); + + default: + return false; + } + } + + } + + private void authenticate() { + try { + if (connection.authenticateWithNone(host.getUsername())) { + finishConnection(); + return; + } + } catch(Exception e) { + Log.d(TAG, "Host does not support 'none' authentication."); + } + + bridge.outputLine(manager.res.getString(R.string.terminal_auth)); + + try { + long pubkeyId = host.getPubkeyId(); + + if (!pubkeysExhausted && + pubkeyId != HostDatabase.PUBKEYID_NEVER && + connection.isAuthMethodAvailable(host.getUsername(), AUTH_PUBLICKEY)) { + + // if explicit pubkey defined for this host, then prompt for password as needed + // otherwise just try all in-memory keys held in terminalmanager + + if (pubkeyId == HostDatabase.PUBKEYID_ANY) { + // try each of the in-memory keys + bridge.outputLine(manager.res + .getString(R.string.terminal_auth_pubkey_any)); + for (Entry entry : manager.loadedKeypairs.entrySet()) { + if (entry.getValue().bean.isConfirmUse() + && !promptForPubkeyUse(entry.getKey())) + continue; + + if (this.tryPublicKey(host.getUsername(), entry.getKey(), + entry.getValue().pair)) { + finishConnection(); + break; + } + } + } else { + bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_specific)); + // use a specific key for this host, as requested + PubkeyBean pubkey = manager.pubkeydb.findPubkeyById(pubkeyId); + + if (pubkey == null) + bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_invalid)); + else + if (tryPublicKey(pubkey)) + finishConnection(); + } + + pubkeysExhausted = true; + } else if (interactiveCanContinue && + connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) { + // this auth method will talk with us using InteractiveCallback interface + // it blocks until authentication finishes + bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki)); + interactiveCanContinue = false; + if(connection.authenticateWithKeyboardInteractive(host.getUsername(), this)) { + finishConnection(); + } else { + bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki_fail)); + } + } else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) { + bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass)); + String password = bridge.getPromptHelper().requestStringPrompt(null, + manager.res.getString(R.string.prompt_password)); + if (password != null + && connection.authenticateWithPassword(host.getUsername(), password)) { + finishConnection(); + } else { + bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass_fail)); + } + } else { + bridge.outputLine(manager.res.getString(R.string.terminal_auth_fail)); + } + } catch (IllegalStateException e) { + Log.e(TAG, "Connection went away while we were trying to authenticate", e); + return; + } catch(Exception e) { + Log.e(TAG, "Problem during handleAuthentication()", e); + } + } + + /** + * Attempt connection with database row pointed to by cursor. + * @param cursor + * @return true for successful authentication + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + * @throws IOException + */ + private boolean tryPublicKey(PubkeyBean pubkey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException { + KeyPair pair = null; + + if(manager.isKeyLoaded(pubkey.getNickname())) { + // load this key from memory if its already there + Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", pubkey.getNickname())); + + if (pubkey.isConfirmUse()) { + if (!promptForPubkeyUse(pubkey.getNickname())) + return false; + } + + pair = manager.getKey(pubkey.getNickname()); + } else { + // otherwise load key from database and prompt for password as needed + String password = null; + if (pubkey.isEncrypted()) { + password = bridge.getPromptHelper().requestStringPrompt(null, + manager.res.getString(R.string.prompt_pubkey_password, pubkey.getNickname())); + + // Something must have interrupted the prompt. + if (password == null) + return false; + } + + if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) { + // load specific key using pem format + pair = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password); + } else { + // load using internal generated format + PrivateKey privKey; + try { + privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(), + pubkey.getType(), password); + } catch (Exception e) { + String message = String.format("Bad password for key '%s'. Authentication failed.", pubkey.getNickname()); + Log.e(TAG, message, e); + bridge.outputLine(message); + return false; + } + + PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); + + // convert key to trilead format + pair = new KeyPair(pubKey, privKey); + Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey)); + } + + Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname())); + + // save this key in memory + manager.addKey(pubkey, pair); + } + + return tryPublicKey(host.getUsername(), pubkey.getNickname(), pair); + } + + private boolean tryPublicKey(String username, String keyNickname, KeyPair pair) throws IOException { + //bridge.outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString())); + boolean success = connection.authenticateWithPublicKey(username, pair); + if(!success) + bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_fail, keyNickname)); + return success; + } + + /** + * Internal method to request actual PTY terminal once we've finished + * authentication. If called before authenticated, it will just fail. + */ + private void finishConnection() { + authenticated = true; + + for (PortForwardBean portForward : portForwards) { + try { + enablePortForward(portForward); + bridge.outputLine(manager.res.getString(R.string.terminal_enable_portfoward, portForward.getDescription())); + } catch (Exception e) { + Log.e(TAG, "Error setting up port forward during connect", e); + } + } + + if (!host.getWantSession()) { + bridge.outputLine(manager.res.getString(R.string.terminal_no_session)); + bridge.onConnected(); + return; + } + + try { + session = connection.openSession(); + + if (!useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) + session.requestAuthAgentForwarding(this); + + session.requestPTY(getEmulation(), columns, rows, width, height, null); + session.startShell(); + + stdin = session.getStdin(); + stdout = session.getStdout(); + stderr = session.getStderr(); + + sessionOpen = true; + + bridge.onConnected(); + } catch (IOException e1) { + Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1); + } + + } + + @Override + public void connect() { + connection = new Connection(host.getHostname(), host.getPort()); + connection.addConnectionMonitor(this); + + try { + connection.setCompression(compression); + } catch (IOException e) { + Log.e(TAG, "Could not enable compression!", e); + } + + try { + /* Uncomment when debugging SSH protocol: + DebugLogger logger = new DebugLogger() { + + public void log(int level, String className, String message) { + Log.d("SSH", message); + } + + }; + Logger.enabled = true; + Logger.logger = logger; + */ + connectionInfo = connection.connect(new HostKeyVerifier()); + connected = true; + + if (connectionInfo.clientToServerCryptoAlgorithm + .equals(connectionInfo.serverToClientCryptoAlgorithm) + && connectionInfo.clientToServerMACAlgorithm + .equals(connectionInfo.serverToClientMACAlgorithm)) { + bridge.outputLine(manager.res.getString(R.string.terminal_using_algorithm, + connectionInfo.clientToServerCryptoAlgorithm, + connectionInfo.clientToServerMACAlgorithm)); + } else { + bridge.outputLine(manager.res.getString( + R.string.terminal_using_c2s_algorithm, + connectionInfo.clientToServerCryptoAlgorithm, + connectionInfo.clientToServerMACAlgorithm)); + + bridge.outputLine(manager.res.getString( + R.string.terminal_using_s2c_algorithm, + connectionInfo.serverToClientCryptoAlgorithm, + connectionInfo.serverToClientMACAlgorithm)); + } + } catch (IOException e) { + Log.e(TAG, "Problem in SSH connection thread during authentication", e); + + // Display the reason in the text. + bridge.outputLine(e.getCause().getMessage()); + + onDisconnect(); + return; + } + + try { + // enter a loop to keep trying until authentication + int tries = 0; + while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) { + authenticate(); + + // sleep to make sure we dont kill system + Thread.sleep(1000); + } + } catch(Exception e) { + Log.e(TAG, "Problem in SSH connection thread during authentication", e); + } + } + + @Override + public void close() { + connected = false; + + if (session != null) { + session.close(); + session = null; + } + + if (connection != null) { + connection.close(); + connection = null; + } + } + + private void onDisconnect() { + close(); + + bridge.dispatchDisconnect(false); + } + + @Override + public void flush() throws IOException { + if (stdin != null) + stdin.flush(); + } + + @Override + public int read(byte[] buffer, int start, int len) throws IOException { + int bytesRead = 0; + + if (session == null) + return 0; + + int newConditions = session.waitForCondition(conditions, 0); + + if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) { + bytesRead = stdout.read(buffer, start, len); + } + + if ((newConditions & ChannelCondition.STDERR_DATA) != 0) { + byte discard[] = new byte[256]; + while (stderr.available() > 0) { + stderr.read(discard); + } + } + + if ((newConditions & ChannelCondition.EOF) != 0) { + onDisconnect(); + throw new IOException("Remote end closed connection"); + } + + return bytesRead; + } + + @Override + public void write(byte[] buffer) throws IOException { + if (stdin != null) + stdin.write(buffer); + } + + @Override + public void write(int c) throws IOException { + if (stdin != null) + stdin.write(c); + } + + @Override + public Map getOptions() { + Map options = new HashMap(); + + options.put("compression", Boolean.toString(compression)); + + return options; + } + + @Override + public void setOptions(Map options) { + if (options.containsKey("compression")) + compression = Boolean.parseBoolean(options.get("compression")); + } + + public static String getProtocolName() { + return PROTOCOL; + } + + @Override + public boolean isSessionOpen() { + return sessionOpen; + } + + @Override + public boolean isConnected() { + return connected; + } + + public void connectionLost(Throwable reason) { + onDisconnect(); + } + + @Override + public boolean canForwardPorts() { + return true; + } + + @Override + public List getPortForwards() { + return portForwards; + } + + @Override + public boolean addPortForward(PortForwardBean portForward) { + return portForwards.add(portForward); + } + + @Override + public boolean removePortForward(PortForwardBean portForward) { + // Make sure we don't have a phantom forwarder. + disablePortForward(portForward); + + return portForwards.remove(portForward); + } + + @Override + public boolean enablePortForward(PortForwardBean portForward) { + if (!portForwards.contains(portForward)) { + Log.e(TAG, "Attempt to enable port forward not in list"); + return false; + } + + if (!authenticated) + return false; + + if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) { + LocalPortForwarder lpf = null; + try { + lpf = connection.createLocalPortForwarder( + new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()), + portForward.getDestAddr(), portForward.getDestPort()); + } catch (Exception e) { + Log.e(TAG, "Could not create local port forward", e); + return false; + } + + if (lpf == null) { + Log.e(TAG, "returned LocalPortForwarder object is null"); + return false; + } + + portForward.setIdentifier(lpf); + portForward.setEnabled(true); + return true; + } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) { + try { + connection.requestRemotePortForwarding("", portForward.getSourcePort(), portForward.getDestAddr(), portForward.getDestPort()); + } catch (Exception e) { + Log.e(TAG, "Could not create remote port forward", e); + return false; + } + + portForward.setEnabled(true); + return true; + } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) { + DynamicPortForwarder dpf = null; + + try { + dpf = connection.createDynamicPortForwarder( + new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort())); + } catch (Exception e) { + Log.e(TAG, "Could not create dynamic port forward", e); + return false; + } + + portForward.setIdentifier(dpf); + portForward.setEnabled(true); + return true; + } else { + // Unsupported type + Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType())); + return false; + } + } + + @Override + public boolean disablePortForward(PortForwardBean portForward) { + if (!portForwards.contains(portForward)) { + Log.e(TAG, "Attempt to disable port forward not in list"); + return false; + } + + if (!authenticated) + return false; + + if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) { + LocalPortForwarder lpf = null; + lpf = (LocalPortForwarder)portForward.getIdentifier(); + + if (!portForward.isEnabled() || lpf == null) { + Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname())); + return false; + } + + portForward.setEnabled(false); + + try { + lpf.close(); + } catch (IOException e) { + Log.e(TAG, "Could not stop local port forwarder, setting enabled to false", e); + return false; + } + + return true; + } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) { + portForward.setEnabled(false); + + try { + connection.cancelRemotePortForwarding(portForward.getSourcePort()); + } catch (IOException e) { + Log.e(TAG, "Could not stop remote port forwarding, setting enabled to false", e); + return false; + } + + return true; + } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) { + DynamicPortForwarder dpf = null; + dpf = (DynamicPortForwarder)portForward.getIdentifier(); + + if (!portForward.isEnabled() || dpf == null) { + Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname())); + return false; + } + + portForward.setEnabled(false); + + try { + dpf.close(); + } catch (IOException e) { + Log.e(TAG, "Could not stop dynamic port forwarder, setting enabled to false", e); + return false; + } + + return true; + } else { + // Unsupported type + Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType())); + return false; + } + } + + @Override + public void setDimensions(int columns, int rows, int width, int height) { + this.columns = columns; + this.rows = rows; + + if (sessionOpen) { + try { + session.resizePTY(columns, rows, width, height); + } catch (IOException e) { + Log.e(TAG, "Couldn't send resize PTY packet", e); + } + } + } + + @Override + public int getDefaultPort() { + return DEFAULT_PORT; + } + + @Override + public String getDefaultNickname(String username, String hostname, int port) { + if (port == DEFAULT_PORT) { + return String.format(Locale.US, "%s@%s", username, hostname); + } else { + return String.format(Locale.US, "%s@%s:%d", username, hostname, port); + } + } + + public static Uri getUri(String input) { + Matcher matcher = hostmask.matcher(input); + + if (!matcher.matches()) + return null; + + StringBuilder sb = new StringBuilder(); + + sb.append(PROTOCOL) + .append("://") + .append(Uri.encode(matcher.group(1))) + .append('@') + .append(matcher.group(2)); + + String portString = matcher.group(4); + int port = DEFAULT_PORT; + if (portString != null) { + try { + port = Integer.parseInt(portString); + if (port < 1 || port > 65535) { + port = DEFAULT_PORT; + } + } catch (NumberFormatException nfe) { + // Keep the default port + } + } + + if (port != DEFAULT_PORT) { + sb.append(':') + .append(port); + } + + sb.append("/#") + .append(Uri.encode(input)); + + Uri uri = Uri.parse(sb.toString()); + + return uri; + } + + /** + * Handle challenges from keyboard-interactive authentication mode. + */ + public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) { + interactiveCanContinue = true; + String[] responses = new String[numPrompts]; + for(int i = 0; i < numPrompts; i++) { + // request response from user for each prompt + responses[i] = bridge.promptHelper.requestStringPrompt(instruction, prompt[i]); + } + return responses; + } + + @Override + public HostBean createHost(Uri uri) { + HostBean host = new HostBean(); + + host.setProtocol(PROTOCOL); + + host.setHostname(uri.getHost()); + + int port = uri.getPort(); + if (port < 0) + port = DEFAULT_PORT; + host.setPort(port); + + host.setUsername(uri.getUserInfo()); + + String nickname = uri.getFragment(); + if (nickname == null || nickname.length() == 0) { + host.setNickname(getDefaultNickname(host.getUsername(), + host.getHostname(), host.getPort())); + } else { + host.setNickname(uri.getFragment()); + } + + return host; + } + + @Override + public void getSelectionArgs(Uri uri, Map selection) { + selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL); + selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment()); + selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost()); + + int port = uri.getPort(); + if (port < 0) + port = DEFAULT_PORT; + selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port)); + selection.put(HostDatabase.FIELD_HOST_USERNAME, uri.getUserInfo()); + } + + @Override + public void setCompression(boolean compression) { + this.compression = compression; + } + + public static String getFormatHint(Context context) { + return String.format("%s@%s:%s", + context.getString(R.string.format_username), + context.getString(R.string.format_hostname), + context.getString(R.string.format_port)); + } + + @Override + public void setUseAuthAgent(String useAuthAgent) { + this.useAuthAgent = useAuthAgent; + } + + public Map retrieveIdentities() { + Map pubKeys = new HashMap(manager.loadedKeypairs.size()); + + for (Entry entry : manager.loadedKeypairs.entrySet()) { + KeyPair pair = entry.getValue().pair; + + try { + PrivateKey privKey = pair.getPrivate(); + if (privKey instanceof RSAPrivateKey) { + RSAPublicKey pubkey = (RSAPublicKey) pair.getPublic(); + pubKeys.put(entry.getKey(), RSASHA1Verify.encodeSSHRSAPublicKey(pubkey)); + } else if (privKey instanceof DSAPrivateKey) { + DSAPublicKey pubkey = (DSAPublicKey) pair.getPublic(); + pubKeys.put(entry.getKey(), DSASHA1Verify.encodeSSHDSAPublicKey(pubkey)); + } else + continue; + } catch (IOException e) { + continue; + } + } + + return pubKeys; + } + + public KeyPair getKeyPair(byte[] publicKey) { + String nickname = manager.getKeyNickname(publicKey); + + if (nickname == null) + return null; + + if (useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) { + Log.e(TAG, ""); + return null; + } else if (useAuthAgent.equals(HostDatabase.AUTHAGENT_CONFIRM) || + manager.loadedKeypairs.get(nickname).bean.isConfirmUse()) { + if (!promptForPubkeyUse(nickname)) + return null; + } + return manager.getKey(nickname); + } + + private boolean promptForPubkeyUse(String nickname) { + Boolean result = bridge.promptHelper.requestBooleanPrompt(null, + manager.res.getString(R.string.prompt_allow_agent_to_use_key, + nickname)); + return result; + } + + public boolean addIdentity(KeyPair pair, String comment, boolean confirmUse, int lifetime) { + PubkeyBean pubkey = new PubkeyBean(); +// pubkey.setType(PubkeyDatabase.KEY_TYPE_IMPORTED); + pubkey.setNickname(comment); + pubkey.setConfirmUse(confirmUse); + pubkey.setLifetime(lifetime); + manager.addKey(pubkey, pair); + return true; + } + + public boolean removeAllIdentities() { + manager.loadedKeypairs.clear(); + return true; + } + + public boolean removeIdentity(byte[] publicKey) { + return manager.removeKey(publicKey); + } + + public boolean isAgentLocked() { + return agentLockPassphrase != null; + } + + public boolean requestAgentUnlock(String unlockPassphrase) { + if (agentLockPassphrase == null) + return false; + + if (agentLockPassphrase.equals(unlockPassphrase)) + agentLockPassphrase = null; + + return agentLockPassphrase == null; + } + + public boolean setAgentLock(String lockPassphrase) { + if (agentLockPassphrase != null) + return false; + + agentLockPassphrase = lockPassphrase; + return true; + } + + /* (non-Javadoc) + * @see org.connectbot.transport.AbsTransport#usesNetwork() + */ + @Override + public boolean usesNetwork() { + return true; + } +} diff --git a/app/src/main/java/org/connectbot/transport/Telnet.java b/app/src/main/java/org/connectbot/transport/Telnet.java new file mode 100644 index 0000000..5fde2f6 --- /dev/null +++ b/app/src/main/java/org/connectbot/transport/Telnet.java @@ -0,0 +1,330 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.connectbot.R; +import org.connectbot.bean.HostBean; +import org.connectbot.service.TerminalBridge; +import org.connectbot.service.TerminalManager; +import org.connectbot.util.HostDatabase; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; +import de.mud.telnet.TelnetProtocolHandler; + +/** + * Telnet transport implementation.
+ * Original idea from the JTA telnet package (de.mud.telnet) + * + * @author Kenny Root + * + */ +public class Telnet extends AbsTransport { + private static final String TAG = "ConnectBot.Telnet"; + private static final String PROTOCOL = "telnet"; + + private static final int DEFAULT_PORT = 23; + + private TelnetProtocolHandler handler; + private Socket socket; + + private InputStream is; + private OutputStream os; + private int width; + private int height; + + private boolean connected = false; + + static final Pattern hostmask; + static { + hostmask = Pattern.compile("^([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE); + } + + public Telnet() { + handler = new TelnetProtocolHandler() { + /** get the current terminal type */ + @Override + public String getTerminalType() { + return getEmulation(); + } + + /** get the current window size */ + @Override + public int[] getWindowSize() { + return new int[] { width, height }; + } + + /** notify about local echo */ + @Override + public void setLocalEcho(boolean echo) { + /* EMPTY */ + } + + /** write data to our back end */ + @Override + public void write(byte[] b) throws IOException { + if (os != null) + os.write(b); + } + + /** sent on IAC EOR (prompt terminator for remote access systems). */ + @Override + public void notifyEndOfRecord() { + } + + @Override + protected String getCharsetName() { + Charset charset = bridge.getCharset(); + if (charset != null) + return charset.name(); + else + return ""; + } + }; + } + + /** + * @param host + * @param bridge + * @param manager + */ + public Telnet(HostBean host, TerminalBridge bridge, TerminalManager manager) { + super(host, bridge, manager); + } + + public static String getProtocolName() { + return PROTOCOL; + } + + @Override + public void connect() { + try { + socket = new Socket(host.getHostname(), host.getPort()); + + connected = true; + + is = socket.getInputStream(); + os = socket.getOutputStream(); + + bridge.onConnected(); + } catch (UnknownHostException e) { + Log.d(TAG, "IO Exception connecting to host", e); + } catch (IOException e) { + Log.d(TAG, "IO Exception connecting to host", e); + } + } + + @Override + public void close() { + connected = false; + if (socket != null) + try { + socket.close(); + socket = null; + } catch (IOException e) { + Log.d(TAG, "Error closing telnet socket.", e); + } + } + + @Override + public void flush() throws IOException { + os.flush(); + } + + @Override + public int getDefaultPort() { + return DEFAULT_PORT; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public boolean isSessionOpen() { + return connected; + } + + @Override + public int read(byte[] buffer, int start, int len) throws IOException { + /* process all already read bytes */ + int n = 0; + + do { + n = handler.negotiate(buffer, start); + if (n > 0) + return n; + } while (n == 0); + + while (n <= 0) { + do { + n = handler.negotiate(buffer, start); + if (n > 0) + return n; + } while (n == 0); + n = is.read(buffer, start, len); + if (n < 0) { + bridge.dispatchDisconnect(false); + throw new IOException("Remote end closed connection."); + } + + handler.inputfeed(buffer, start, n); + n = handler.negotiate(buffer, start); + } + return n; + } + + @Override + public void write(byte[] buffer) throws IOException { + try { + if (os != null) + os.write(buffer); + } catch (SocketException e) { + bridge.dispatchDisconnect(false); + } + } + + @Override + public void write(int c) throws IOException { + try { + if (os != null) + os.write(c); + } catch (SocketException e) { + bridge.dispatchDisconnect(false); + } + } + + @Override + public void setDimensions(int columns, int rows, int width, int height) { + try { + handler.setWindowSize(columns, rows); + } catch (IOException e) { + Log.e(TAG, "Couldn't resize remote terminal", e); + } + } + + @Override + public String getDefaultNickname(String username, String hostname, int port) { + if (port == DEFAULT_PORT) { + return String.format("%s", hostname); + } else { + return String.format("%s:%d", hostname, port); + } + } + + public static Uri getUri(String input) { + Matcher matcher = hostmask.matcher(input); + + if (!matcher.matches()) + return null; + + StringBuilder sb = new StringBuilder(); + + sb.append(PROTOCOL) + .append("://") + .append(matcher.group(1)); + + String portString = matcher.group(3); + int port = DEFAULT_PORT; + if (portString != null) { + try { + port = Integer.parseInt(portString); + if (port < 1 || port > 65535) { + port = DEFAULT_PORT; + } + } catch (NumberFormatException nfe) { + // Keep the default port + } + } + + if (port != DEFAULT_PORT) { + sb.append(':'); + sb.append(port); + } + + sb.append("/#") + .append(Uri.encode(input)); + + Uri uri = Uri.parse(sb.toString()); + + return uri; + } + + @Override + public HostBean createHost(Uri uri) { + HostBean host = new HostBean(); + + host.setProtocol(PROTOCOL); + + host.setHostname(uri.getHost()); + + int port = uri.getPort(); + if (port < 0) + port = DEFAULT_PORT; + host.setPort(port); + + String nickname = uri.getFragment(); + if (nickname == null || nickname.length() == 0) { + host.setNickname(getDefaultNickname(host.getUsername(), + host.getHostname(), host.getPort())); + } else { + host.setNickname(uri.getFragment()); + } + + return host; + } + + @Override + public void getSelectionArgs(Uri uri, Map selection) { + selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL); + selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment()); + selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost()); + + int port = uri.getPort(); + if (port < 0) + port = DEFAULT_PORT; + selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port)); + } + + public static String getFormatHint(Context context) { + return String.format("%s:%s", + context.getString(R.string.format_hostname), + context.getString(R.string.format_port)); + } + + /* (non-Javadoc) + * @see org.connectbot.transport.AbsTransport#usesNetwork() + */ + @Override + public boolean usesNetwork() { + return true; + } +} diff --git a/app/src/main/java/org/connectbot/transport/TransportFactory.java b/app/src/main/java/org/connectbot/transport/TransportFactory.java new file mode 100644 index 0000000..72e5e08 --- /dev/null +++ b/app/src/main/java/org/connectbot/transport/TransportFactory.java @@ -0,0 +1,132 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.transport; + +import java.util.HashMap; +import java.util.Map; + +import org.connectbot.bean.HostBean; +import org.connectbot.util.HostDatabase; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + + +/** + * @author Kenny Root + * + */ +public class TransportFactory { + private static final String TAG = "ConnectBot.TransportFactory"; + + private static String[] transportNames = { + SSH.getProtocolName(), + Telnet.getProtocolName(), + Local.getProtocolName(), + }; + + /** + * @param protocol + * @return + */ + public static AbsTransport getTransport(String protocol) { + if (SSH.getProtocolName().equals(protocol)) { + return new SSH(); + } else if (Telnet.getProtocolName().equals(protocol)) { + return new Telnet(); + } else if (Local.getProtocolName().equals(protocol)) { + return new Local(); + } else { + return null; + } + } + + public static Uri getUri(String scheme, String input) { + Log.d("TransportFactory", String.format( + "Attempting to discover URI for scheme=%s on input=%s", scheme, + input)); + if (SSH.getProtocolName().equals(scheme)) + return SSH.getUri(input); + else if (Telnet.getProtocolName().equals(scheme)) + return Telnet.getUri(input); + else if (Local.getProtocolName().equals(scheme)) { + Log.d("TransportFactory", "Got to the local parsing area"); + return Local.getUri(input); + } else + return null; + } + + public static String[] getTransportNames() { + return transportNames; + } + + public static boolean isSameTransportType(AbsTransport a, AbsTransport b) { + if (a == null || b == null) + return false; + + return a.getClass().equals(b.getClass()); + } + + public static boolean canForwardPorts(String protocol) { + // TODO uh, make this have less knowledge about its children + if (SSH.getProtocolName().equals(protocol)) { + return true; + } else { + return false; + } + } + + /** + * @param protocol text name of protocol + * @param context + * @return expanded format hint + */ + public static String getFormatHint(String protocol, Context context) { + if (SSH.getProtocolName().equals(protocol)) { + return SSH.getFormatHint(context); + } else if (Telnet.getProtocolName().equals(protocol)) { + return Telnet.getFormatHint(context); + } else if (Local.getProtocolName().equals(protocol)) { + return Local.getFormatHint(context); + } else { + return AbsTransport.getFormatHint(context); + } + } + + /** + * @param hostdb Handle to HostDatabase + * @param uri URI to target server + * @param host HostBean in which to put the results + * @return true when host was found + */ + public static HostBean findHost(HostDatabase hostdb, Uri uri) { + AbsTransport transport = getTransport(uri.getScheme()); + + Map selection = new HashMap(); + + transport.getSelectionArgs(uri, selection); + if (selection.size() == 0) { + Log.e(TAG, String.format("Transport %s failed to do something useful with URI=%s", + uri.getScheme(), uri.toString())); + throw new IllegalStateException("Failed to get needed selection arguments"); + } + + return hostdb.findHost(selection); + } +} diff --git a/app/src/main/java/org/connectbot/util/Colors.java b/app/src/main/java/org/connectbot/util/Colors.java new file mode 100644 index 0000000..ff88d68 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/Colors.java @@ -0,0 +1,91 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +/** + * @author Kenny Root + * + */ +public class Colors { + public final static Integer[] defaults = new Integer[] { + 0xff000000, // black + 0xffcc0000, // red + 0xff00cc00, // green + 0xffcccc00, // brown + 0xff0000cc, // blue + 0xffcc00cc, // purple + 0xff00cccc, // cyan + 0xffcccccc, // light grey + 0xff444444, // dark grey + 0xffff4444, // light red + 0xff44ff44, // light green + 0xffffff44, // yellow + 0xff4444ff, // light blue + 0xffff44ff, // light purple + 0xff44ffff, // light cyan + 0xffffffff, // white + 0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, + 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, + 0xff005fd7, 0xff005fff, 0xff008700, 0xff00875f, 0xff008787, + 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, + 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff, 0xff00d700, + 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, + 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, + 0xff00ffff, 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, + 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, + 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff, 0xff5f8700, 0xff5f875f, + 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, + 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff, + 0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, + 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, + 0xff5fffd7, 0xff5fffff, 0xff870000, 0xff87005f, 0xff870087, + 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, + 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff, 0xff878700, + 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, + 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, + 0xff87afff, 0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, + 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, + 0xff87ffaf, 0xff87ffd7, 0xff87ffff, 0xffaf0000, 0xffaf005f, + 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, + 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff, + 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, + 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, + 0xffafafd7, 0xffafafff, 0xffafd700, 0xffafd75f, 0xffafd787, + 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, + 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff, 0xffd70000, + 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, + 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, + 0xffd75fff, 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, + 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, + 0xffd7afaf, 0xffd7afd7, 0xffd7afff, 0xffd7d700, 0xffd7d75f, + 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, + 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff, + 0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, + 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, + 0xffff5fd7, 0xffff5fff, 0xffff8700, 0xffff875f, 0xffff8787, + 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, + 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff, 0xffffd700, + 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, + 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, + 0xffffffff, 0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, + 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, + 0xff626262, 0xff6c6c6c, 0xff767676, 0xff808080, 0xff8a8a8a, + 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, + 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee, + }; +} diff --git a/app/src/main/java/org/connectbot/util/EastAsianWidth.java b/app/src/main/java/org/connectbot/util/EastAsianWidth.java new file mode 100644 index 0000000..0e274b5 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/EastAsianWidth.java @@ -0,0 +1,75 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import android.graphics.Paint; +import android.text.AndroidCharacter; + +/** + * @author Kenny Root + * + */ +public abstract class EastAsianWidth { + public static EastAsianWidth getInstance() { + if (PreferenceConstants.PRE_FROYO) + return PreFroyo.Holder.sInstance; + else + return FroyoAndBeyond.Holder.sInstance; + } + + /** + * @param charArray + * @param i + * @param position + * @param wideAttribute + */ + public abstract void measure(char[] charArray, int start, int end, + byte[] wideAttribute, Paint paint, int charWidth); + + private static class PreFroyo extends EastAsianWidth { + private static final int BUFFER_SIZE = 4096; + private float[] mWidths = new float[BUFFER_SIZE]; + + private static class Holder { + private static final PreFroyo sInstance = new PreFroyo(); + } + + @Override + public void measure(char[] charArray, int start, int end, + byte[] wideAttribute, Paint paint, int charWidth) { + paint.getTextWidths(charArray, start, end, mWidths); + final int N = end - start; + for (int i = 0; i < N; i++) + wideAttribute[i] = (byte) (((int)mWidths[i] != charWidth) ? + AndroidCharacter.EAST_ASIAN_WIDTH_WIDE : + AndroidCharacter.EAST_ASIAN_WIDTH_NARROW); + } + } + + private static class FroyoAndBeyond extends EastAsianWidth { + private static class Holder { + private static final FroyoAndBeyond sInstance = new FroyoAndBeyond(); + } + + @Override + public void measure(char[] charArray, int start, int end, + byte[] wideAttribute, Paint paint, int charWidth) { + AndroidCharacter.getEastAsianWidths(charArray, start, end - start, wideAttribute); + } + } +} diff --git a/app/src/main/java/org/connectbot/util/Encryptor.java b/app/src/main/java/org/connectbot/util/Encryptor.java new file mode 100644 index 0000000..9d21454 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/Encryptor.java @@ -0,0 +1,205 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +/** + * This class is from: + * + * Encryptor.java + * Copyright 2008 Zach Scrivena + * zachscrivena@gmail.com + * http://zs.freeshell.org/ + */ + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + + +/** + * Perform AES-128 encryption. + */ +public final class Encryptor +{ + /** name of the character set to use for converting between characters and bytes */ + private static final String CHARSET_NAME = "UTF-8"; + + /** random number generator algorithm */ + private static final String RNG_ALGORITHM = "SHA1PRNG"; + + /** message digest algorithm (must be sufficiently long to provide the key and initialization vector) */ + private static final String DIGEST_ALGORITHM = "SHA-256"; + + /** key algorithm (must be compatible with CIPHER_ALGORITHM) */ + private static final String KEY_ALGORITHM = "AES"; + + /** cipher algorithm (must be compatible with KEY_ALGORITHM) */ + private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + + + /** + * Private constructor that should never be called. + */ + private Encryptor() + {} + + + /** + * Encrypt the specified cleartext using the given password. + * With the correct salt, number of iterations, and password, the decrypt() method reverses + * the effect of this method. + * This method generates and uses a random salt, and the user-specified number of iterations + * and password to create a 16-byte secret key and 16-byte initialization vector. + * The secret key and initialization vector are then used in the AES-128 cipher to encrypt + * the given cleartext. + * + * @param salt + * salt that was used in the encryption (to be populated) + * @param iterations + * number of iterations to use in salting + * @param password + * password to be used for encryption + * @param cleartext + * cleartext to be encrypted + * @return + * ciphertext + * @throws Exception + * on any error encountered in encryption + */ + public static byte[] encrypt( + final byte[] salt, + final int iterations, + final String password, + final byte[] cleartext) + throws Exception + { + /* generate salt randomly */ + SecureRandom.getInstance(RNG_ALGORITHM).nextBytes(salt); + + /* compute key and initialization vector */ + final MessageDigest shaDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); + byte[] pw = password.getBytes(CHARSET_NAME); + + for (int i = 0; i < iterations; i++) + { + /* add salt */ + final byte[] salted = new byte[pw.length + salt.length]; + System.arraycopy(pw, 0, salted, 0, pw.length); + System.arraycopy(salt, 0, salted, pw.length, salt.length); + Arrays.fill(pw, (byte) 0x00); + + /* compute SHA-256 digest */ + shaDigest.reset(); + pw = shaDigest.digest(salted); + Arrays.fill(salted, (byte) 0x00); + } + + /* extract the 16-byte key and initialization vector from the SHA-256 digest */ + final byte[] key = new byte[16]; + final byte[] iv = new byte[16]; + System.arraycopy(pw, 0, key, 0, 16); + System.arraycopy(pw, 16, iv, 0, 16); + Arrays.fill(pw, (byte) 0x00); + + /* perform AES-128 encryption */ + final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + + cipher.init( + Cipher.ENCRYPT_MODE, + new SecretKeySpec(key, KEY_ALGORITHM), + new IvParameterSpec(iv)); + + Arrays.fill(key, (byte) 0x00); + Arrays.fill(iv, (byte) 0x00); + + return cipher.doFinal(cleartext); + } + + + /** + * Decrypt the specified ciphertext using the given password. + * With the correct salt, number of iterations, and password, this method reverses the effect + * of the encrypt() method. + * This method uses the user-specified salt, number of iterations, and password + * to recreate the 16-byte secret key and 16-byte initialization vector. + * The secret key and initialization vector are then used in the AES-128 cipher to decrypt + * the given ciphertext. + * + * @param salt + * salt to be used in decryption + * @param iterations + * number of iterations to use in salting + * @param password + * password to be used for decryption + * @param ciphertext + * ciphertext to be decrypted + * @return + * cleartext + * @throws Exception + * on any error encountered in decryption + */ + public static byte[] decrypt( + final byte[] salt, + final int iterations, + final String password, + final byte[] ciphertext) + throws Exception + { + /* compute key and initialization vector */ + final MessageDigest shaDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); + byte[] pw = password.getBytes(CHARSET_NAME); + + for (int i = 0; i < iterations; i++) + { + /* add salt */ + final byte[] salted = new byte[pw.length + salt.length]; + System.arraycopy(pw, 0, salted, 0, pw.length); + System.arraycopy(salt, 0, salted, pw.length, salt.length); + Arrays.fill(pw, (byte) 0x00); + + /* compute SHA-256 digest */ + shaDigest.reset(); + pw = shaDigest.digest(salted); + Arrays.fill(salted, (byte) 0x00); + } + + /* extract the 16-byte key and initialization vector from the SHA-256 digest */ + final byte[] key = new byte[16]; + final byte[] iv = new byte[16]; + System.arraycopy(pw, 0, key, 0, 16); + System.arraycopy(pw, 16, iv, 0, 16); + Arrays.fill(pw, (byte) 0x00); + + /* perform AES-128 decryption */ + final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + + cipher.init( + Cipher.DECRYPT_MODE, + new SecretKeySpec(key, KEY_ALGORITHM), + new IvParameterSpec(iv)); + + Arrays.fill(key, (byte) 0x00); + Arrays.fill(iv, (byte) 0x00); + + return cipher.doFinal(ciphertext); + } +} diff --git a/app/src/main/java/org/connectbot/util/EntropyDialog.java b/app/src/main/java/org/connectbot/util/EntropyDialog.java new file mode 100644 index 0000000..4498ce2 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/EntropyDialog.java @@ -0,0 +1,50 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import org.connectbot.R; + +import android.app.Dialog; +import android.content.Context; +import android.view.View; + +public class EntropyDialog extends Dialog implements OnEntropyGatheredListener { + + public EntropyDialog(Context context) { + super(context); + + this.setContentView(R.layout.dia_gatherentropy); + this.setTitle(R.string.pubkey_gather_entropy); + + ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this); + } + + public EntropyDialog(Context context, View view) { + super(context); + + this.setContentView(view); + this.setTitle(R.string.pubkey_gather_entropy); + + ((EntropyView) findViewById(R.id.entropy)).addOnEntropyGatheredListener(this); + } + + public void onEntropyGathered(byte[] entropy) { + this.dismiss(); + } + +} diff --git a/app/src/main/java/org/connectbot/util/EntropyView.java b/app/src/main/java/org/connectbot/util/EntropyView.java new file mode 100644 index 0000000..c988673 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/EntropyView.java @@ -0,0 +1,169 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.util.Vector; + +import org.connectbot.R; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.Paint.FontMetrics; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +public class EntropyView extends View { + private static final int SHA1_MAX_BYTES = 20; + private static final int MILLIS_BETWEEN_INPUTS = 50; + + private Paint mPaint; + private FontMetrics mFontMetrics; + private boolean mFlipFlop; + private long mLastTime; + private Vector listeners; + + private byte[] mEntropy; + private int mEntropyByteIndex; + private int mEntropyBitIndex; + + private int splitText = 0; + + private float lastX = 0.0f, lastY = 0.0f; + + public EntropyView(Context context) { + super(context); + + setUpEntropy(); + } + + public EntropyView(Context context, AttributeSet attrs) { + super(context, attrs); + + setUpEntropy(); + } + + private void setUpEntropy() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTypeface(Typeface.DEFAULT); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setTextSize(16); + mPaint.setColor(Color.WHITE); + mFontMetrics = mPaint.getFontMetrics(); + + mEntropy = new byte[SHA1_MAX_BYTES]; + mEntropyByteIndex = 0; + mEntropyBitIndex = 0; + + listeners = new Vector(); + } + + public void addOnEntropyGatheredListener(OnEntropyGatheredListener listener) { + listeners.add(listener); + } + + public void removeOnEntropyGatheredListener(OnEntropyGatheredListener listener) { + listeners.remove(listener); + } + + @Override + public void onDraw(Canvas c) { + String prompt = String.format(getResources().getString(R.string.pubkey_touch_prompt), + (int)(100.0 * (mEntropyByteIndex / 20.0)) + (int)(5.0 * (mEntropyBitIndex / 8.0))); + if (splitText > 0 || + mPaint.measureText(prompt) > (getWidth() * 0.8)) { + if (splitText == 0) + splitText = prompt.indexOf(" ", prompt.length() / 2); + + c.drawText(prompt.substring(0, splitText), + getWidth() / 2.0f, + getHeight() / 2.0f + (mPaint.ascent() + mPaint.descent()), + mPaint); + c.drawText(prompt.substring(splitText), + getWidth() / 2.0f, + getHeight() / 2.0f - (mPaint.ascent() + mPaint.descent()), + mPaint); + } else { + c.drawText(prompt, + getWidth() / 2.0f, + getHeight() / 2.0f - (mFontMetrics.ascent + mFontMetrics.descent) / 2, + mPaint); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mEntropyByteIndex >= SHA1_MAX_BYTES + || lastX == event.getX() + || lastY == event.getY()) + return true; + + // Only get entropy every 200 milliseconds to ensure the user has moved around. + long now = System.currentTimeMillis(); + if ((now - mLastTime) < MILLIS_BETWEEN_INPUTS) + return true; + else + mLastTime = now; + + byte input; + + lastX = event.getX(); + lastY = event.getY(); + + // Get the lowest 4 bits of each X, Y input and concat to the entropy-gathering + // string. + if (mFlipFlop) + input = (byte)((((int)lastX & 0x0F) << 4) | ((int)lastY & 0x0F)); + else + input = (byte)((((int)lastY & 0x0F) << 4) | ((int)lastX & 0x0F)); + mFlipFlop = !mFlipFlop; + + for (int i = 0; i < 4 && mEntropyByteIndex < SHA1_MAX_BYTES; i++) { + if ((input & 0x3) == 0x1) { + mEntropy[mEntropyByteIndex] <<= 1; + mEntropy[mEntropyByteIndex] |= 1; + mEntropyBitIndex++; + input >>= 2; + } else if ((input & 0x3) == 0x2) { + mEntropy[mEntropyByteIndex] <<= 1; + mEntropyBitIndex++; + input >>= 2; + } + + if (mEntropyBitIndex >= 8) { + mEntropyBitIndex = 0; + mEntropyByteIndex++; + } + } + + // SHA1PRNG only keeps 160 bits of entropy. + if (mEntropyByteIndex >= SHA1_MAX_BYTES) { + for (OnEntropyGatheredListener listener: listeners) { + listener.onEntropyGathered(mEntropy); + } + } + + invalidate(); + + return true; + } +} diff --git a/app/src/main/java/org/connectbot/util/HelpTopicView.java b/app/src/main/java/org/connectbot/util/HelpTopicView.java new file mode 100644 index 0000000..0cbc267 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/HelpTopicView.java @@ -0,0 +1,62 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import org.connectbot.HelpActivity; + +import android.content.Context; +import android.util.AttributeSet; +import android.webkit.WebSettings; +import android.webkit.WebView; + +/** + * @author Kenny Root + * + */ +public class HelpTopicView extends WebView { + public HelpTopicView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initialize(); + } + + public HelpTopicView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public HelpTopicView(Context context) { + super(context); + initialize(); + } + + private void initialize() { + WebSettings wSet = getSettings(); + wSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS); + wSet.setUseWideViewPort(false); + } + + public HelpTopicView setTopic(String topic) { + String path = String.format("file:///android_asset/%s/%s%s", + HelpActivity.HELPDIR, topic, HelpActivity.SUFFIX); + loadUrl(path); + + computeScroll(); + + return this; + } +} diff --git a/app/src/main/java/org/connectbot/util/HostDatabase.java b/app/src/main/java/org/connectbot/util/HostDatabase.java new file mode 100644 index 0000000..2a92bab --- /dev/null +++ b/app/src/main/java/org/connectbot/util/HostDatabase.java @@ -0,0 +1,766 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.connectbot.bean.HostBean; +import org.connectbot.bean.PortForwardBean; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; + +import com.trilead.ssh2.KnownHosts; + +/** + * Contains information about various SSH hosts, include public hostkey if known + * from previous sessions. + * + * @author jsharkey + */ +public class HostDatabase extends RobustSQLiteOpenHelper { + + public final static String TAG = "ConnectBot.HostDatabase"; + + public final static String DB_NAME = "hosts"; + public final static int DB_VERSION = 22; + + public final static String TABLE_HOSTS = "hosts"; + public final static String FIELD_HOST_NICKNAME = "nickname"; + public final static String FIELD_HOST_PROTOCOL = "protocol"; + public final static String FIELD_HOST_USERNAME = "username"; + public final static String FIELD_HOST_HOSTNAME = "hostname"; + public final static String FIELD_HOST_PORT = "port"; + public final static String FIELD_HOST_HOSTKEYALGO = "hostkeyalgo"; + public final static String FIELD_HOST_HOSTKEY = "hostkey"; + public final static String FIELD_HOST_LASTCONNECT = "lastconnect"; + public final static String FIELD_HOST_COLOR = "color"; + public final static String FIELD_HOST_USEKEYS = "usekeys"; + public final static String FIELD_HOST_USEAUTHAGENT = "useauthagent"; + public final static String FIELD_HOST_POSTLOGIN = "postlogin"; + public final static String FIELD_HOST_PUBKEYID = "pubkeyid"; + public final static String FIELD_HOST_WANTSESSION = "wantsession"; + public final static String FIELD_HOST_DELKEY = "delkey"; + public final static String FIELD_HOST_FONTSIZE = "fontsize"; + public final static String FIELD_HOST_COMPRESSION = "compression"; + public final static String FIELD_HOST_ENCODING = "encoding"; + public final static String FIELD_HOST_STAYCONNECTED = "stayconnected"; + + public final static String TABLE_PORTFORWARDS = "portforwards"; + public final static String FIELD_PORTFORWARD_HOSTID = "hostid"; + public final static String FIELD_PORTFORWARD_NICKNAME = "nickname"; + public final static String FIELD_PORTFORWARD_TYPE = "type"; + public final static String FIELD_PORTFORWARD_SOURCEPORT = "sourceport"; + public final static String FIELD_PORTFORWARD_DESTADDR = "destaddr"; + public final static String FIELD_PORTFORWARD_DESTPORT = "destport"; + + public final static String TABLE_COLORS = "colors"; + public final static String FIELD_COLOR_SCHEME = "scheme"; + public final static String FIELD_COLOR_NUMBER = "number"; + public final static String FIELD_COLOR_VALUE = "value"; + + public final static String TABLE_COLOR_DEFAULTS = "colorDefaults"; + public final static String FIELD_COLOR_FG = "fg"; + public final static String FIELD_COLOR_BG = "bg"; + + public final static int DEFAULT_FG_COLOR = 7; + public final static int DEFAULT_BG_COLOR = 0; + + public final static String COLOR_RED = "red"; + public final static String COLOR_GREEN = "green"; + public final static String COLOR_BLUE = "blue"; + public final static String COLOR_GRAY = "gray"; + + public final static String PORTFORWARD_LOCAL = "local"; + public final static String PORTFORWARD_REMOTE = "remote"; + public final static String PORTFORWARD_DYNAMIC4 = "dynamic4"; + public final static String PORTFORWARD_DYNAMIC5 = "dynamic5"; + + public final static String DELKEY_DEL = "del"; + public final static String DELKEY_BACKSPACE = "backspace"; + + public final static String AUTHAGENT_NO = "no"; + public final static String AUTHAGENT_CONFIRM = "confirm"; + public final static String AUTHAGENT_YES = "yes"; + + public final static String ENCODING_DEFAULT = Charset.defaultCharset().name(); + + public final static long PUBKEYID_NEVER = -2; + public final static long PUBKEYID_ANY = -1; + + public static final int DEFAULT_COLOR_SCHEME = 0; + + // Table creation strings + public static final String CREATE_TABLE_COLOR_DEFAULTS = + "CREATE TABLE " + TABLE_COLOR_DEFAULTS + + " (" + FIELD_COLOR_SCHEME + " INTEGER NOT NULL, " + + FIELD_COLOR_FG + " INTEGER NOT NULL DEFAULT " + DEFAULT_FG_COLOR + ", " + + FIELD_COLOR_BG + " INTEGER NOT NULL DEFAULT " + DEFAULT_BG_COLOR + ")"; + public static final String CREATE_TABLE_COLOR_DEFAULTS_INDEX = + "CREATE INDEX " + TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index ON " + + TABLE_COLOR_DEFAULTS + " (" + FIELD_COLOR_SCHEME + ");"; + + private static final String WHERE_SCHEME_AND_COLOR = FIELD_COLOR_SCHEME + " = ? AND " + + FIELD_COLOR_NUMBER + " = ?"; + + static { + addTableName(TABLE_HOSTS); + addTableName(TABLE_PORTFORWARDS); + addIndexName(TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index"); + addTableName(TABLE_COLORS); + addIndexName(TABLE_COLORS + FIELD_COLOR_SCHEME + "index"); + addTableName(TABLE_COLOR_DEFAULTS); + addIndexName(TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index"); + } + + public static final Object[] dbLock = new Object[0]; + + public HostDatabase(Context context) { + super(context, DB_NAME, null, DB_VERSION); + + getWritableDatabase().close(); + } + + @Override + public void onCreate(SQLiteDatabase db) { + super.onCreate(db); + + db.execSQL("CREATE TABLE " + TABLE_HOSTS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_HOST_NICKNAME + " TEXT, " + + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh', " + + FIELD_HOST_USERNAME + " TEXT, " + + FIELD_HOST_HOSTNAME + " TEXT, " + + FIELD_HOST_PORT + " INTEGER, " + + FIELD_HOST_HOSTKEYALGO + " TEXT, " + + FIELD_HOST_HOSTKEY + " BLOB, " + + FIELD_HOST_LASTCONNECT + " INTEGER, " + + FIELD_HOST_COLOR + " TEXT, " + + FIELD_HOST_USEKEYS + " TEXT, " + + FIELD_HOST_USEAUTHAGENT + " TEXT, " + + FIELD_HOST_POSTLOGIN + " TEXT, " + + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY + ", " + + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "', " + + FIELD_HOST_FONTSIZE + " INTEGER, " + + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "', " + + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "', " + + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "', " + + FIELD_HOST_STAYCONNECTED + " TEXT)"); + + db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_PORTFORWARD_HOSTID + " INTEGER, " + + FIELD_PORTFORWARD_NICKNAME + " TEXT, " + + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", " + + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, " + + FIELD_PORTFORWARD_DESTADDR + " TEXT, " + + FIELD_PORTFORWARD_DESTPORT + " TEXT)"); + + db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON " + + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");"); + + db.execSQL("CREATE TABLE " + TABLE_COLORS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_COLOR_NUMBER + " INTEGER, " + + FIELD_COLOR_VALUE + " INTEGER, " + + FIELD_COLOR_SCHEME + " INTEGER)"); + + db.execSQL("CREATE INDEX " + TABLE_COLORS + FIELD_COLOR_SCHEME + "index ON " + + TABLE_COLORS + " (" + FIELD_COLOR_SCHEME + ");"); + + db.execSQL(CREATE_TABLE_COLOR_DEFAULTS); + db.execSQL(CREATE_TABLE_COLOR_DEFAULTS_INDEX); + } + + @Override + public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException { + // Versions of the database before the Android Market release will be + // shot without warning. + if (oldVersion <= 9) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_HOSTS); + onCreate(db); + return; + } + + switch (oldVersion) { + case 10: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY); + case 11: + db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_PORTFORWARD_HOSTID + " INTEGER, " + + FIELD_PORTFORWARD_NICKNAME + " TEXT, " + + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", " + + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, " + + FIELD_PORTFORWARD_DESTADDR + " TEXT, " + + FIELD_PORTFORWARD_DESTPORT + " INTEGER)"); + case 12: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "'"); + case 13: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "'"); + case 14: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "'"); + case 15: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh'"); + case 16: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "'"); + case 17: + db.execSQL("CREATE INDEX " + TABLE_PORTFORWARDS + FIELD_PORTFORWARD_HOSTID + "index ON " + + TABLE_PORTFORWARDS + " (" + FIELD_PORTFORWARD_HOSTID + ");"); + + // Add colors + db.execSQL("CREATE TABLE " + TABLE_COLORS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_COLOR_NUMBER + " INTEGER, " + + FIELD_COLOR_VALUE + " INTEGER, " + + FIELD_COLOR_SCHEME + " INTEGER)"); + db.execSQL("CREATE INDEX " + TABLE_COLORS + FIELD_COLOR_SCHEME + "index ON " + + TABLE_COLORS + " (" + FIELD_COLOR_SCHEME + ");"); + case 18: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_USEAUTHAGENT + " TEXT DEFAULT '" + AUTHAGENT_NO + "'"); + case 19: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_STAYCONNECTED + " TEXT"); + case 20: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_FONTSIZE + " INTEGER"); + case 21: + db.execSQL("DROP TABLE " + TABLE_COLOR_DEFAULTS); + db.execSQL(CREATE_TABLE_COLOR_DEFAULTS); + db.execSQL(CREATE_TABLE_COLOR_DEFAULTS_INDEX); + } + } + + /** + * Touch a specific host to update its "last connected" field. + * @param nickname Nickname field of host to update + */ + public void touchHost(HostBean host) { + long now = System.currentTimeMillis() / 1000; + + ContentValues values = new ContentValues(); + values.put(FIELD_HOST_LASTCONNECT, now); + + synchronized (dbLock) { + SQLiteDatabase db = this.getWritableDatabase(); + + db.update(TABLE_HOSTS, values, "_id = ?", new String[] { String.valueOf(host.getId()) }); + } + } + + /** + * Create a new host using the given parameters. + */ + public HostBean saveHost(HostBean host) { + long id; + + synchronized (dbLock) { + SQLiteDatabase db = this.getWritableDatabase(); + + id = db.insert(TABLE_HOSTS, null, host.getValues()); + } + + host.setId(id); + + return host; + } + + /** + * Update a field in a host record. + */ + public boolean updateFontSize(HostBean host) { + long id = host.getId(); + if (id < 0) + return false; + + ContentValues updates = new ContentValues(); + updates.put(FIELD_HOST_FONTSIZE, host.getFontSize()); + + synchronized (dbLock) { + SQLiteDatabase db = getWritableDatabase(); + + db.update(TABLE_HOSTS, updates, "_id = ?", + new String[] { String.valueOf(id) }); + + } + + return true; + } + + /** + * Delete a specific host by its _id value. + */ + public void deleteHost(HostBean host) { + if (host.getId() < 0) + return; + + synchronized (dbLock) { + SQLiteDatabase db = this.getWritableDatabase(); + db.delete(TABLE_HOSTS, "_id = ?", new String[] { String.valueOf(host.getId()) }); + } + } + + /** + * Return a cursor that contains information about all known hosts. + * @param sortColors If true, sort by color, otherwise sort by nickname. + */ + public List getHosts(boolean sortColors) { + String sortField = sortColors ? FIELD_HOST_COLOR : FIELD_HOST_NICKNAME; + List hosts; + + synchronized (dbLock) { + SQLiteDatabase db = this.getReadableDatabase(); + + Cursor c = db.query(TABLE_HOSTS, null, null, null, null, null, sortField + " ASC"); + + hosts = createHostBeans(c); + + c.close(); + } + + return hosts; + } + + /** + * @param hosts + * @param c + */ + private List createHostBeans(Cursor c) { + List hosts = new LinkedList(); + + final int COL_ID = c.getColumnIndexOrThrow("_id"), + COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_HOST_NICKNAME), + COL_PROTOCOL = c.getColumnIndexOrThrow(FIELD_HOST_PROTOCOL), + COL_USERNAME = c.getColumnIndexOrThrow(FIELD_HOST_USERNAME), + COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME), + COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT), + COL_LASTCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_LASTCONNECT), + COL_COLOR = c.getColumnIndexOrThrow(FIELD_HOST_COLOR), + COL_USEKEYS = c.getColumnIndexOrThrow(FIELD_HOST_USEKEYS), + COL_USEAUTHAGENT = c.getColumnIndexOrThrow(FIELD_HOST_USEAUTHAGENT), + COL_POSTLOGIN = c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN), + COL_PUBKEYID = c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID), + COL_WANTSESSION = c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION), + COL_DELKEY = c.getColumnIndexOrThrow(FIELD_HOST_DELKEY), + COL_FONTSIZE = c.getColumnIndexOrThrow(FIELD_HOST_FONTSIZE), + COL_COMPRESSION = c.getColumnIndexOrThrow(FIELD_HOST_COMPRESSION), + COL_ENCODING = c.getColumnIndexOrThrow(FIELD_HOST_ENCODING), + COL_STAYCONNECTED = c.getColumnIndexOrThrow(FIELD_HOST_STAYCONNECTED); + + + while (c.moveToNext()) { + HostBean host = new HostBean(); + + host.setId(c.getLong(COL_ID)); + host.setNickname(c.getString(COL_NICKNAME)); + host.setProtocol(c.getString(COL_PROTOCOL)); + host.setUsername(c.getString(COL_USERNAME)); + host.setHostname(c.getString(COL_HOSTNAME)); + host.setPort(c.getInt(COL_PORT)); + host.setLastConnect(c.getLong(COL_LASTCONNECT)); + host.setColor(c.getString(COL_COLOR)); + host.setUseKeys(Boolean.valueOf(c.getString(COL_USEKEYS))); + host.setUseAuthAgent(c.getString(COL_USEAUTHAGENT)); + host.setPostLogin(c.getString(COL_POSTLOGIN)); + host.setPubkeyId(c.getLong(COL_PUBKEYID)); + host.setWantSession(Boolean.valueOf(c.getString(COL_WANTSESSION))); + host.setDelKey(c.getString(COL_DELKEY)); + host.setFontSize(c.getInt(COL_FONTSIZE)); + host.setCompression(Boolean.valueOf(c.getString(COL_COMPRESSION))); + host.setEncoding(c.getString(COL_ENCODING)); + host.setStayConnected(Boolean.valueOf(c.getString(COL_STAYCONNECTED))); + + hosts.add(host); + } + + return hosts; + } + + /** + * @param c + * @return + */ + private HostBean getFirstHostBean(Cursor c) { + HostBean host = null; + + List hosts = createHostBeans(c); + if (hosts.size() > 0) + host = hosts.get(0); + + c.close(); + + return host; + } + + /** + * @param nickname + * @param protocol + * @param username + * @param hostname + * @param hostname2 + * @param port + * @return + */ + public HostBean findHost(Map selection) { + StringBuilder selectionBuilder = new StringBuilder(); + + Iterator> i = selection.entrySet().iterator(); + + List selectionValuesList = new LinkedList(); + int n = 0; + while (i.hasNext()) { + Entry entry = i.next(); + + if (entry.getValue() == null) + continue; + + if (n++ > 0) + selectionBuilder.append(" AND "); + + selectionBuilder.append(entry.getKey()) + .append(" = ?"); + + selectionValuesList.add(entry.getValue()); + } + + String selectionValues[] = new String[selectionValuesList.size()]; + selectionValuesList.toArray(selectionValues); + selectionValuesList = null; + + HostBean host; + + synchronized (dbLock) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor c = db.query(TABLE_HOSTS, null, + selectionBuilder.toString(), + selectionValues, + null, null, null); + + host = getFirstHostBean(c); + } + + return host; + } + + /** + * @param hostId + * @return + */ + public HostBean findHostById(long hostId) { + HostBean host; + + synchronized (dbLock) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor c = db.query(TABLE_HOSTS, null, + "_id = ?", new String[] { String.valueOf(hostId) }, + null, null, null); + + host = getFirstHostBean(c); + } + + return host; + } + + /** + * Record the given hostkey into database under this nickname. + * @param hostname + * @param port + * @param hostkeyalgo + * @param hostkey + */ + public void saveKnownHost(String hostname, int port, String hostkeyalgo, byte[] hostkey) { + ContentValues values = new ContentValues(); + values.put(FIELD_HOST_HOSTKEYALGO, hostkeyalgo); + values.put(FIELD_HOST_HOSTKEY, hostkey); + + synchronized (dbLock) { + SQLiteDatabase db = getReadableDatabase(); + + db.update(TABLE_HOSTS, values, + FIELD_HOST_HOSTNAME + " = ? AND " + FIELD_HOST_PORT + " = ?", + new String[] { hostname, String.valueOf(port) }); + Log.d(TAG, String.format("Finished saving hostkey information for '%s'", hostname)); + } + } + + /** + * Build list of known hosts for Trilead library. + * @return + */ + public KnownHosts getKnownHosts() { + KnownHosts known = new KnownHosts(); + + synchronized (dbLock) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor c = db.query(TABLE_HOSTS, new String[] { FIELD_HOST_HOSTNAME, + FIELD_HOST_PORT, FIELD_HOST_HOSTKEYALGO, FIELD_HOST_HOSTKEY }, + null, null, null, null, null); + + if (c != null) { + int COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME), + COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT), + COL_HOSTKEYALGO = c.getColumnIndexOrThrow(FIELD_HOST_HOSTKEYALGO), + COL_HOSTKEY = c.getColumnIndexOrThrow(FIELD_HOST_HOSTKEY); + + while (c.moveToNext()) { + String hostname = c.getString(COL_HOSTNAME), + hostkeyalgo = c.getString(COL_HOSTKEYALGO); + int port = c.getInt(COL_PORT); + byte[] hostkey = c.getBlob(COL_HOSTKEY); + + if (hostkeyalgo == null || hostkeyalgo.length() == 0) continue; + if (hostkey == null || hostkey.length == 0) continue; + + try { + known.addHostkey(new String[] { String.format("%s:%d", hostname, port) }, hostkeyalgo, hostkey); + } catch(Exception e) { + Log.e(TAG, "Problem while adding a known host from database", e); + } + } + + c.close(); + } + } + + return known; + } + + /** + * Unset any hosts using a pubkey ID that has been deleted. + * @param pubkeyId + */ + public void stopUsingPubkey(long pubkeyId) { + if (pubkeyId < 0) return; + + ContentValues values = new ContentValues(); + values.put(FIELD_HOST_PUBKEYID, PUBKEYID_ANY); + + synchronized (dbLock) { + SQLiteDatabase db = this.getWritableDatabase(); + + db.update(TABLE_HOSTS, values, FIELD_HOST_PUBKEYID + " = ?", new String[] { String.valueOf(pubkeyId) }); + } + + Log.d(TAG, String.format("Set all hosts using pubkey id %d to -1", pubkeyId)); + } + + /* + * Methods for dealing with port forwards attached to hosts + */ + + /** + * Returns a list of all the port forwards associated with a particular host ID. + * @param host the host for which we want the port forward list + * @return port forwards associated with host ID + */ + public List getPortForwardsForHost(HostBean host) { + List portForwards = new LinkedList(); + + synchronized (dbLock) { + SQLiteDatabase db = this.getReadableDatabase(); + + Cursor c = db.query(TABLE_PORTFORWARDS, new String[] { + "_id", FIELD_PORTFORWARD_NICKNAME, FIELD_PORTFORWARD_TYPE, FIELD_PORTFORWARD_SOURCEPORT, + FIELD_PORTFORWARD_DESTADDR, FIELD_PORTFORWARD_DESTPORT }, + FIELD_PORTFORWARD_HOSTID + " = ?", new String[] { String.valueOf(host.getId()) }, + null, null, null); + + while (c.moveToNext()) { + PortForwardBean pfb = new PortForwardBean( + c.getInt(0), + host.getId(), + c.getString(1), + c.getString(2), + c.getInt(3), + c.getString(4), + c.getInt(5)); + portForwards.add(pfb); + } + + c.close(); + } + + return portForwards; + } + + /** + * Update the parameters of a port forward in the database. + * @param pfb {@link PortForwardBean} to save + * @return true on success + */ + public boolean savePortForward(PortForwardBean pfb) { + boolean success = false; + + synchronized (dbLock) { + SQLiteDatabase db = getWritableDatabase(); + + if (pfb.getId() < 0) { + long id = db.insert(TABLE_PORTFORWARDS, null, pfb.getValues()); + pfb.setId(id); + success = true; + } else { + if (db.update(TABLE_PORTFORWARDS, pfb.getValues(), "_id = ?", new String[] { String.valueOf(pfb.getId()) }) > 0) + success = true; + } + } + + return success; + } + + /** + * Deletes a port forward from the database. + * @param pfb {@link PortForwardBean} to delete + */ + public void deletePortForward(PortForwardBean pfb) { + if (pfb.getId() < 0) + return; + + synchronized (dbLock) { + SQLiteDatabase db = this.getWritableDatabase(); + db.delete(TABLE_PORTFORWARDS, "_id = ?", new String[] { String.valueOf(pfb.getId()) }); + } + } + + public Integer[] getColorsForScheme(int scheme) { + Integer[] colors = Colors.defaults.clone(); + + synchronized (dbLock) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor c = db.query(TABLE_COLORS, new String[] { + FIELD_COLOR_NUMBER, FIELD_COLOR_VALUE }, + FIELD_COLOR_SCHEME + " = ?", + new String[] { String.valueOf(scheme) }, + null, null, null); + + while (c.moveToNext()) { + colors[c.getInt(0)] = new Integer(c.getInt(1)); + } + + c.close(); + } + + return colors; + } + + public void setColorForScheme(int scheme, int number, int value) { + final SQLiteDatabase db; + + final String[] whereArgs = new String[] { String.valueOf(scheme), String.valueOf(number) }; + + if (value == Colors.defaults[number]) { + synchronized (dbLock) { + db = getWritableDatabase(); + + db.delete(TABLE_COLORS, + WHERE_SCHEME_AND_COLOR, whereArgs); + } + } else { + final ContentValues values = new ContentValues(); + values.put(FIELD_COLOR_VALUE, value); + + synchronized (dbLock) { + db = getWritableDatabase(); + + final int rowsAffected = db.update(TABLE_COLORS, values, + WHERE_SCHEME_AND_COLOR, whereArgs); + + if (rowsAffected == 0) { + values.put(FIELD_COLOR_SCHEME, scheme); + values.put(FIELD_COLOR_NUMBER, number); + db.insert(TABLE_COLORS, null, values); + } + } + } + } + + public void setGlobalColor(int number, int value) { + setColorForScheme(DEFAULT_COLOR_SCHEME, number, value); + } + + public int[] getDefaultColorsForScheme(int scheme) { + int[] colors = new int[] { DEFAULT_FG_COLOR, DEFAULT_BG_COLOR }; + + synchronized (dbLock) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor c = db.query(TABLE_COLOR_DEFAULTS, + new String[] { FIELD_COLOR_FG, FIELD_COLOR_BG }, + FIELD_COLOR_SCHEME + " = ?", + new String[] { String.valueOf(scheme) }, + null, null, null); + + if (c.moveToFirst()) { + colors[0] = c.getInt(0); + colors[1] = c.getInt(1); + } + + c.close(); + } + + return colors; + } + + public int[] getGlobalDefaultColors() { + return getDefaultColorsForScheme(DEFAULT_COLOR_SCHEME); + } + + public void setDefaultColorsForScheme(int scheme, int fg, int bg) { + SQLiteDatabase db; + + String schemeWhere = null; + String[] whereArgs; + + schemeWhere = FIELD_COLOR_SCHEME + " = ?"; + whereArgs = new String[] { String.valueOf(scheme) }; + + ContentValues values = new ContentValues(); + values.put(FIELD_COLOR_FG, fg); + values.put(FIELD_COLOR_BG, bg); + + synchronized (dbLock) { + db = getWritableDatabase(); + + int rowsAffected = db.update(TABLE_COLOR_DEFAULTS, values, + schemeWhere, whereArgs); + + if (rowsAffected == 0) { + values.put(FIELD_COLOR_SCHEME, scheme); + db.insert(TABLE_COLOR_DEFAULTS, null, values); + } + } + } +} diff --git a/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java b/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java new file mode 100644 index 0000000..ef33797 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/OnDbWrittenListener.java @@ -0,0 +1,26 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2010 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +/** + * @author kroot + * + */ +public interface OnDbWrittenListener { + public void onDbWritten(); +} diff --git a/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java b/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java new file mode 100644 index 0000000..5debd65 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java @@ -0,0 +1,22 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +public interface OnEntropyGatheredListener { + void onEntropyGathered(byte[] entropy); +} diff --git a/app/src/main/java/org/connectbot/util/PreferenceConstants.java b/app/src/main/java/org/connectbot/util/PreferenceConstants.java new file mode 100644 index 0000000..e9fb06c --- /dev/null +++ b/app/src/main/java/org/connectbot/util/PreferenceConstants.java @@ -0,0 +1,90 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import android.os.Build; + +/** + * @author Kenny Root + * + */ +public class PreferenceConstants { + public static final int SDK_INT = Integer.parseInt(Build.VERSION.SDK); + public static final boolean PRE_ECLAIR = SDK_INT < 5; + public static final boolean PRE_FROYO = SDK_INT < 8; + public static final boolean PRE_HONEYCOMB = SDK_INT < 11; + + public static final String MEMKEYS = "memkeys"; + public static final String UPDATE = "update"; + + public static final String UPDATE_DAILY = "Daily"; + public static final String UPDATE_WEEKLY = "Weekly"; + public static final String UPDATE_NEVER = "Never"; + + public static final String LAST_CHECKED = "lastchecked"; + + public static final String SCROLLBACK = "scrollback"; + + public static final String EMULATION = "emulation"; + + public static final String ROTATION = "rotation"; + + public static final String ROTATION_DEFAULT = "Default"; + public static final String ROTATION_LANDSCAPE = "Force landscape"; + public static final String ROTATION_PORTRAIT = "Force portrait"; + public static final String ROTATION_AUTOMATIC = "Automatic"; + + public static final String FULLSCREEN = "fullscreen"; + + public static final String KEYMODE = "keymode"; + + public static final String KEYMODE_RIGHT = "Use right-side keys"; + public static final String KEYMODE_LEFT = "Use left-side keys"; + + public static final String CAMERA = "camera"; + + public static final String CAMERA_CTRLA_SPACE = "Ctrl+A then Space"; + public static final String CAMERA_CTRLA = "Ctrl+A"; + public static final String CAMERA_ESC = "Esc"; + public static final String CAMERA_ESC_A = "Esc+A"; + + public static final String KEEP_ALIVE = "keepalive"; + + public static final String WIFI_LOCK = "wifilock"; + + public static final String BUMPY_ARROWS = "bumpyarrows"; + + public static final String EULA = "eula"; + + public static final String SORT_BY_COLOR = "sortByColor"; + + public static final String BELL = "bell"; + public static final String BELL_VOLUME = "bellVolume"; + public static final String BELL_VIBRATE = "bellVibrate"; + public static final String BELL_NOTIFICATION = "bellNotification"; + public static final float DEFAULT_BELL_VOLUME = 0.25f; + + public static final String CONNECTION_PERSIST = "connPersist"; + + public static final String SHIFT_FKEYS = "shiftfkeys"; + public static final String CTRL_FKEYS = "ctrlfkeys"; + public static final String VOLUME_FONT = "volumefont"; + + /* Backup identifiers */ + public static final String BACKUP_PREF_KEY = "prefs"; +} diff --git a/app/src/main/java/org/connectbot/util/PubkeyDatabase.java b/app/src/main/java/org/connectbot/util/PubkeyDatabase.java new file mode 100644 index 0000000..a8993cb --- /dev/null +++ b/app/src/main/java/org/connectbot/util/PubkeyDatabase.java @@ -0,0 +1,329 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.util.LinkedList; +import java.util.List; + +import org.connectbot.bean.PubkeyBean; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +/** + * Public Key Encryption database. Contains private and public key pairs + * for public key authentication. + * + * @author Kenny Root + */ +public class PubkeyDatabase extends RobustSQLiteOpenHelper { + public final static String TAG = "ConnectBot.PubkeyDatabase"; + + public final static String DB_NAME = "pubkeys"; + public final static int DB_VERSION = 2; + + public final static String TABLE_PUBKEYS = "pubkeys"; + public final static String FIELD_PUBKEY_NICKNAME = "nickname"; + public final static String FIELD_PUBKEY_TYPE = "type"; + public final static String FIELD_PUBKEY_PRIVATE = "private"; + public final static String FIELD_PUBKEY_PUBLIC = "public"; + public final static String FIELD_PUBKEY_ENCRYPTED = "encrypted"; + public final static String FIELD_PUBKEY_STARTUP = "startup"; + public final static String FIELD_PUBKEY_CONFIRMUSE = "confirmuse"; + public final static String FIELD_PUBKEY_LIFETIME = "lifetime"; + + public final static String KEY_TYPE_RSA = "RSA", + KEY_TYPE_DSA = "DSA", + KEY_TYPE_IMPORTED = "IMPORTED", + KEY_TYPE_EC = "EC"; + + private Context context; + + static { + addTableName(TABLE_PUBKEYS); + } + + public PubkeyDatabase(Context context) { + super(context, DB_NAME, null, DB_VERSION); + + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + super.onCreate(db); + + db.execSQL("CREATE TABLE " + TABLE_PUBKEYS + + " (_id INTEGER PRIMARY KEY, " + + FIELD_PUBKEY_NICKNAME + " TEXT, " + + FIELD_PUBKEY_TYPE + " TEXT, " + + FIELD_PUBKEY_PRIVATE + " BLOB, " + + FIELD_PUBKEY_PUBLIC + " BLOB, " + + FIELD_PUBKEY_ENCRYPTED + " INTEGER, " + + FIELD_PUBKEY_STARTUP + " INTEGER, " + + FIELD_PUBKEY_CONFIRMUSE + " INTEGER DEFAULT 0, " + + FIELD_PUBKEY_LIFETIME + " INTEGER DEFAULT 0)"); + } + + @Override + public void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException { + switch (oldVersion) { + case 1: + db.execSQL("ALTER TABLE " + TABLE_PUBKEYS + + " ADD COLUMN " + FIELD_PUBKEY_CONFIRMUSE + " INTEGER DEFAULT 0"); + db.execSQL("ALTER TABLE " + TABLE_PUBKEYS + + " ADD COLUMN " + FIELD_PUBKEY_LIFETIME + " INTEGER DEFAULT 0"); + } + } + + /** + * Delete a specific host by its _id value. + */ + public void deletePubkey(PubkeyBean pubkey) { + HostDatabase hostdb = new HostDatabase(context); + hostdb.stopUsingPubkey(pubkey.getId()); + hostdb.close(); + + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_PUBKEYS, "_id = ?", new String[] { Long.toString(pubkey.getId()) }); + db.close(); + } + + /** + * Return a cursor that contains information about all known hosts. + */ + /* + public Cursor allPubkeys() { + SQLiteDatabase db = this.getReadableDatabase(); + return db.query(TABLE_PUBKEYS, new String[] { "_id", + FIELD_PUBKEY_NICKNAME, FIELD_PUBKEY_TYPE, FIELD_PUBKEY_PRIVATE, + FIELD_PUBKEY_PUBLIC, FIELD_PUBKEY_ENCRYPTED, FIELD_PUBKEY_STARTUP }, + null, null, null, null, null); + }*/ + + public List allPubkeys() { + return getPubkeys(null, null); + } + + public List getAllStartPubkeys() { + return getPubkeys(FIELD_PUBKEY_STARTUP + " = 1 AND " + FIELD_PUBKEY_ENCRYPTED + " = 0", null); + } + + private List getPubkeys(String selection, String[] selectionArgs) { + SQLiteDatabase db = getReadableDatabase(); + + List pubkeys = new LinkedList(); + + Cursor c = db.query(TABLE_PUBKEYS, null, selection, selectionArgs, null, null, null); + + if (c != null) { + final int COL_ID = c.getColumnIndexOrThrow("_id"), + COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME), + COL_TYPE = c.getColumnIndexOrThrow(FIELD_PUBKEY_TYPE), + COL_PRIVATE = c.getColumnIndexOrThrow(FIELD_PUBKEY_PRIVATE), + COL_PUBLIC = c.getColumnIndexOrThrow(FIELD_PUBKEY_PUBLIC), + COL_ENCRYPTED = c.getColumnIndexOrThrow(FIELD_PUBKEY_ENCRYPTED), + COL_STARTUP = c.getColumnIndexOrThrow(FIELD_PUBKEY_STARTUP), + COL_CONFIRMUSE = c.getColumnIndexOrThrow(FIELD_PUBKEY_CONFIRMUSE), + COL_LIFETIME = c.getColumnIndexOrThrow(FIELD_PUBKEY_LIFETIME); + + while (c.moveToNext()) { + PubkeyBean pubkey = new PubkeyBean(); + + pubkey.setId(c.getLong(COL_ID)); + pubkey.setNickname(c.getString(COL_NICKNAME)); + pubkey.setType(c.getString(COL_TYPE)); + pubkey.setPrivateKey(c.getBlob(COL_PRIVATE)); + pubkey.setPublicKey(c.getBlob(COL_PUBLIC)); + pubkey.setEncrypted(c.getInt(COL_ENCRYPTED) > 0); + pubkey.setStartup(c.getInt(COL_STARTUP) > 0); + pubkey.setConfirmUse(c.getInt(COL_CONFIRMUSE) > 0); + pubkey.setLifetime(c.getInt(COL_LIFETIME)); + + pubkeys.add(pubkey); + } + + c.close(); + } + + db.close(); + + return pubkeys; + } + + /** + * @param hostId + * @return + */ + public PubkeyBean findPubkeyById(long pubkeyId) { + SQLiteDatabase db = getReadableDatabase(); + + Cursor c = db.query(TABLE_PUBKEYS, null, + "_id = ?", new String[] { String.valueOf(pubkeyId) }, + null, null, null); + + PubkeyBean pubkey = null; + + if (c != null) { + if (c.moveToFirst()) + pubkey = createPubkeyBean(c); + + c.close(); + } + + db.close(); + + return pubkey; + } + + private PubkeyBean createPubkeyBean(Cursor c) { + PubkeyBean pubkey = new PubkeyBean(); + + pubkey.setId(c.getLong(c.getColumnIndexOrThrow("_id"))); + pubkey.setNickname(c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME))); + pubkey.setType(c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_TYPE))); + pubkey.setPrivateKey(c.getBlob(c.getColumnIndexOrThrow(FIELD_PUBKEY_PRIVATE))); + pubkey.setPublicKey(c.getBlob(c.getColumnIndexOrThrow(FIELD_PUBKEY_PUBLIC))); + pubkey.setEncrypted(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_ENCRYPTED)) > 0); + pubkey.setStartup(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_STARTUP)) > 0); + pubkey.setConfirmUse(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_CONFIRMUSE)) > 0); + pubkey.setLifetime(c.getInt(c.getColumnIndexOrThrow(FIELD_PUBKEY_LIFETIME))); + + return pubkey; + } + + /** + * Pull all values for a given column as a list of Strings, probably for use + * in a ListPreference. Sorted by _id ascending. + */ + public List allValues(String column) { + List list = new LinkedList(); + + SQLiteDatabase db = this.getReadableDatabase(); + Cursor c = db.query(TABLE_PUBKEYS, new String[] { "_id", column }, + null, null, null, null, "_id ASC"); + + if (c != null) { + int COL = c.getColumnIndexOrThrow(column); + + while (c.moveToNext()) + list.add(c.getString(COL)); + + c.close(); + } + + db.close(); + + return list; + } + + public String getNickname(long id) { + String nickname = null; + + SQLiteDatabase db = this.getReadableDatabase(); + Cursor c = db.query(TABLE_PUBKEYS, new String[] { "_id", + FIELD_PUBKEY_NICKNAME }, "_id = ?", + new String[] { Long.toString(id) }, null, null, null); + + if (c != null) { + if (c.moveToFirst()) + nickname = c.getString(c.getColumnIndexOrThrow(FIELD_PUBKEY_NICKNAME)); + + c.close(); + } + + db.close(); + + return nickname; + } + +/* + public void setOnStart(long id, boolean onStart) { + + SQLiteDatabase db = this.getWritableDatabase(); + + ContentValues values = new ContentValues(); + values.put(FIELD_PUBKEY_STARTUP, onStart ? 1 : 0); + + db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { Long.toString(id) }); + + } + + public boolean changePassword(long id, String oldPassword, String newPassword) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException { + SQLiteDatabase db = this.getWritableDatabase(); + + Cursor c = db.query(TABLE_PUBKEYS, new String[] { FIELD_PUBKEY_TYPE, + FIELD_PUBKEY_PRIVATE, FIELD_PUBKEY_ENCRYPTED }, + "_id = ?", new String[] { String.valueOf(id) }, + null, null, null); + + if (!c.moveToFirst()) + return false; + + String keyType = c.getString(0); + byte[] encPriv = c.getBlob(1); + c.close(); + + PrivateKey priv; + try { + priv = PubkeyUtils.decodePrivate(encPriv, keyType, oldPassword); + } catch (InvalidKeyException e) { + return false; + } catch (BadPaddingException e) { + return false; + } catch (InvalidKeySpecException e) { + return false; + } + + ContentValues values = new ContentValues(); + values.put(FIELD_PUBKEY_PRIVATE, PubkeyUtils.getEncodedPrivate(priv, newPassword)); + values.put(FIELD_PUBKEY_ENCRYPTED, newPassword.length() > 0 ? 1 : 0); + db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { String.valueOf(id) }); + + return true; + } + */ + + /** + * @param pubkey + */ + public PubkeyBean savePubkey(PubkeyBean pubkey) { + SQLiteDatabase db = this.getWritableDatabase(); + boolean success = false; + + ContentValues values = pubkey.getValues(); + + if (pubkey.getId() > 0) { + values.remove("_id"); + if (db.update(TABLE_PUBKEYS, values, "_id = ?", new String[] { String.valueOf(pubkey.getId()) }) > 0) + success = true; + } + + if (!success) { + long id = db.insert(TABLE_PUBKEYS, null, pubkey.getValues()); + pubkey.setId(id); + } + + db.close(); + + return pubkey; + } +} diff --git a/app/src/main/java/org/connectbot/util/PubkeyUtils.java b/app/src/main/java/org/connectbot/util/PubkeyUtils.java new file mode 100644 index 0000000..e7922bd --- /dev/null +++ b/app/src/main/java/org/connectbot/util/PubkeyUtils.java @@ -0,0 +1,352 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.keyczar.jce.EcCore; + +import android.util.Log; + +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.crypto.SimpleDERReader; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + +public class PubkeyUtils { + private static final String TAG = "PubkeyUtils"; + + public static final String PKCS8_START = "-----BEGIN PRIVATE KEY-----"; + public static final String PKCS8_END = "-----END PRIVATE KEY-----"; + + // Size in bytes of salt to use. + private static final int SALT_SIZE = 8; + + // Number of iterations for password hashing. PKCS#5 recommends 1000 + private static final int ITERATIONS = 1000; + + // Cannot be instantiated + private PubkeyUtils() { + } + + public static String formatKey(Key key){ + String algo = key.getAlgorithm(); + String fmt = key.getFormat(); + byte[] encoded = key.getEncoded(); + return "Key[algorithm=" + algo + ", format=" + fmt + + ", bytes=" + encoded.length + "]"; + } + + public static byte[] sha256(byte[] data) throws NoSuchAlgorithmException { + return MessageDigest.getInstance("SHA-256").digest(data); + } + + public static byte[] cipher(int mode, byte[] data, byte[] secret) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + SecretKeySpec secretKeySpec = new SecretKeySpec(sha256(secret), "AES"); + Cipher c = Cipher.getInstance("AES"); + c.init(mode, secretKeySpec); + return c.doFinal(data); + } + + public static byte[] encrypt(byte[] cleartext, String secret) throws Exception { + byte[] salt = new byte[SALT_SIZE]; + + byte[] ciphertext = Encryptor.encrypt(salt, ITERATIONS, secret, cleartext); + + byte[] complete = new byte[salt.length + ciphertext.length]; + + System.arraycopy(salt, 0, complete, 0, salt.length); + System.arraycopy(ciphertext, 0, complete, salt.length, ciphertext.length); + + Arrays.fill(salt, (byte) 0x00); + Arrays.fill(ciphertext, (byte) 0x00); + + return complete; + } + + public static byte[] decrypt(byte[] saltAndCiphertext, String secret) throws Exception { + try { + byte[] salt = new byte[SALT_SIZE]; + byte[] ciphertext = new byte[saltAndCiphertext.length - salt.length]; + + System.arraycopy(saltAndCiphertext, 0, salt, 0, salt.length); + System.arraycopy(saltAndCiphertext, salt.length, ciphertext, 0, ciphertext.length); + + return Encryptor.decrypt(salt, ITERATIONS, secret, ciphertext); + } catch (Exception e) { + Log.d("decrypt", "Could not decrypt with new method", e); + // We might be using the old encryption method. + return cipher(Cipher.DECRYPT_MODE, saltAndCiphertext, secret.getBytes()); + } + } + + public static byte[] getEncodedPrivate(PrivateKey pk, String secret) throws Exception { + final byte[] encoded = pk.getEncoded(); + if (secret == null || secret.length() == 0) { + return encoded; + } + return encrypt(pk.getEncoded(), secret); + } + + public static PrivateKey decodePrivate(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException { + PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance(keyType); + return kf.generatePrivate(privKeySpec); + } + + public static PrivateKey decodePrivate(byte[] encoded, String keyType, String secret) throws Exception { + if (secret != null && secret.length() > 0) + return decodePrivate(decrypt(encoded, secret), keyType); + else + return decodePrivate(encoded, keyType); + } + + public static PublicKey decodePublic(byte[] encoded, String keyType) throws NoSuchAlgorithmException, InvalidKeySpecException { + X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance(keyType); + return kf.generatePublic(pubKeySpec); + } + + static String getAlgorithmForOid(String oid) throws NoSuchAlgorithmException { + if ("1.2.840.10045.2.1".equals(oid)) { + return "EC"; + } else if ("1.2.840.113549.1.1.1".equals(oid)) { + return "RSA"; + } else if ("1.2.840.10040.4.1".equals(oid)) { + return "DSA"; + } else { + throw new NoSuchAlgorithmException("Unknown algorithm OID " + oid); + } + } + + static String getOidFromPkcs8Encoded(byte[] encoded) throws NoSuchAlgorithmException { + if (encoded == null) { + throw new NoSuchAlgorithmException("encoding is null"); + } + + try { + SimpleDERReader reader = new SimpleDERReader(encoded); + reader.resetInput(reader.readSequenceAsByteArray()); + reader.readInt(); + reader.resetInput(reader.readSequenceAsByteArray()); + return reader.readOid(); + } catch (IOException e) { + Log.w(TAG, "Could not read OID", e); + throw new NoSuchAlgorithmException("Could not read key", e); + } + } + + public static KeyPair recoverKeyPair(byte[] encoded) throws NoSuchAlgorithmException, + InvalidKeySpecException { + final String algo = getAlgorithmForOid(getOidFromPkcs8Encoded(encoded)); + + final KeySpec privKeySpec = new PKCS8EncodedKeySpec(encoded); + + final KeyFactory kf = KeyFactory.getInstance(algo); + final PrivateKey priv = kf.generatePrivate(privKeySpec); + + return new KeyPair(recoverPublicKey(kf, priv), priv); + } + + static PublicKey recoverPublicKey(KeyFactory kf, PrivateKey priv) + throws NoSuchAlgorithmException, InvalidKeySpecException { + if (priv instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey rsaPriv = (RSAPrivateCrtKey) priv; + return kf.generatePublic(new RSAPublicKeySpec(rsaPriv.getModulus(), rsaPriv + .getPublicExponent())); + } else if (priv instanceof DSAPrivateKey) { + DSAPrivateKey dsaPriv = (DSAPrivateKey) priv; + DSAParams params = dsaPriv.getParams(); + + // Calculate public key Y + BigInteger y = params.getG().modPow(dsaPriv.getX(), params.getP()); + + return kf.generatePublic(new DSAPublicKeySpec(y, params.getP(), params.getQ(), params + .getG())); + } else if (priv instanceof ECPrivateKey) { + ECPrivateKey ecPriv = (ECPrivateKey) priv; + ECParameterSpec params = ecPriv.getParams(); + + // Calculate public key Y + ECPoint generator = params.getGenerator(); + BigInteger[] wCoords = EcCore.multiplyPointA(new BigInteger[] { generator.getAffineX(), + generator.getAffineY() }, ecPriv.getS(), params); + ECPoint w = new ECPoint(wCoords[0], wCoords[1]); + + return kf.generatePublic(new ECPublicKeySpec(w, params)); + } else { + throw new NoSuchAlgorithmException("Key type must be RSA, DSA, or EC"); + } + } + + /* + * OpenSSH compatibility methods + */ + + public static String convertToOpenSSHFormat(PublicKey pk, String origNickname) throws IOException, InvalidKeyException { + String nickname = origNickname; + if (nickname == null) + nickname = "connectbot@android"; + + if (pk instanceof RSAPublicKey) { + String data = "ssh-rsa "; + data += String.valueOf(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pk))); + return data + " " + nickname; + } else if (pk instanceof DSAPublicKey) { + String data = "ssh-dss "; + data += String.valueOf(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pk))); + return data + " " + nickname; + } else if (pk instanceof ECPublicKey) { + ECPublicKey ecPub = (ECPublicKey) pk; + String keyType = ECDSASHA2Verify.getCurveName(ecPub.getParams().getCurve().getField().getFieldSize()); + String keyData = String.valueOf(Base64.encode(ECDSASHA2Verify.encodeSSHECDSAPublicKey(ecPub))); + return ECDSASHA2Verify.ECDSA_SHA2_PREFIX + keyType + " " + keyData + " " + nickname; + } + + throw new InvalidKeyException("Unknown key type"); + } + + /* + * OpenSSH compatibility methods + */ + + /** + * @param trileadKey + * @return OpenSSH-encoded pubkey + */ + public static byte[] extractOpenSSHPublic(KeyPair pair) { + try { + PublicKey pubKey = pair.getPublic(); + if (pubKey instanceof RSAPublicKey) { + return RSASHA1Verify.encodeSSHRSAPublicKey((RSAPublicKey) pair.getPublic()); + } else if (pubKey instanceof DSAPublicKey) { + return DSASHA1Verify.encodeSSHDSAPublicKey((DSAPublicKey) pair.getPublic()); + } else if (pubKey instanceof ECPublicKey) { + return ECDSASHA2Verify.encodeSSHECDSAPublicKey((ECPublicKey) pair.getPublic()); + } else { + return null; + } + } catch (IOException e) { + return null; + } + } + + public static String exportPEM(PrivateKey key, String secret) throws NoSuchAlgorithmException, InvalidParameterSpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, InvalidKeySpecException, IllegalBlockSizeException, IOException { + StringBuilder sb = new StringBuilder(); + + byte[] data = key.getEncoded(); + + sb.append(PKCS8_START); + sb.append('\n'); + + if (secret != null) { + byte[] salt = new byte[8]; + SecureRandom random = new SecureRandom(); + random.nextBytes(salt); + + PBEParameterSpec defParams = new PBEParameterSpec(salt, 1); + AlgorithmParameters params = AlgorithmParameters.getInstance(key.getAlgorithm()); + + params.init(defParams); + + PBEKeySpec pbeSpec = new PBEKeySpec(secret.toCharArray()); + + SecretKeyFactory keyFact = SecretKeyFactory.getInstance(key.getAlgorithm()); + Cipher cipher = Cipher.getInstance(key.getAlgorithm()); + cipher.init(Cipher.WRAP_MODE, keyFact.generateSecret(pbeSpec), params); + + byte[] wrappedKey = cipher.wrap(key); + + EncryptedPrivateKeyInfo pinfo = new EncryptedPrivateKeyInfo(params, wrappedKey); + + data = pinfo.getEncoded(); + + sb.append("Proc-Type: 4,ENCRYPTED\n"); + sb.append("DEK-Info: DES-EDE3-CBC,"); + sb.append(encodeHex(salt)); + sb.append("\n\n"); + } + + int i = sb.length(); + sb.append(Base64.encode(data)); + for (i += 63; i < sb.length(); i += 64) { + sb.insert(i, "\n"); + } + + sb.append('\n'); + sb.append(PKCS8_END); + sb.append('\n'); + + return sb.toString(); + } + + private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + protected static String encodeHex(byte[] bytes) { + final char[] hex = new char[bytes.length * 2]; + + int i = 0; + for (byte b : bytes) { + hex[i++] = HEX_DIGITS[(b >> 4) & 0x0f]; + hex[i++] = HEX_DIGITS[b & 0x0f]; + } + + return String.valueOf(hex); + } +} diff --git a/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java b/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java new file mode 100644 index 0000000..abdd991 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java @@ -0,0 +1,133 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import java.util.LinkedList; +import java.util.List; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteDatabase.CursorFactory; + +/** + * @author Kenny Root + * + */ +public abstract class RobustSQLiteOpenHelper extends SQLiteOpenHelper { + private static List mTableNames = new LinkedList(); + private static List mIndexNames = new LinkedList(); + + public RobustSQLiteOpenHelper(Context context, String name, + CursorFactory factory, int version) { + super(context, name, factory, version); + } + + protected static void addTableName(String tableName) { + mTableNames.add(tableName); + } + + protected static void addIndexName(String indexName) { + mIndexNames.add(indexName); + } + + @Override + public void onCreate(SQLiteDatabase db) { + dropAllTables(db); + } + + @Override + public final void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + try { + onRobustUpgrade(db, oldVersion, newVersion); + } catch (SQLiteException e) { + // The database has entered an unknown state. Try to recover. + try { + regenerateTables(db); + } catch (SQLiteException e2) { + dropAndCreateTables(db); + } + } + } + + public abstract void onRobustUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) throws SQLiteException; + + private void regenerateTables(SQLiteDatabase db) { + dropAllTablesWithPrefix(db, "OLD_"); + + for (String tableName : mTableNames) + db.execSQL("ALTER TABLE " + tableName + " RENAME TO OLD_" + + tableName); + + onCreate(db); + + for (String tableName : mTableNames) + repopulateTable(db, tableName); + + dropAllTablesWithPrefix(db, "OLD_"); + } + + private void repopulateTable(SQLiteDatabase db, String tableName) { + String columns = getTableColumnNames(db, tableName); + + StringBuilder sb = new StringBuilder(); + sb.append("INSERT INTO ") + .append(tableName) + .append(" (") + .append(columns) + .append(") SELECT ") + .append(columns) + .append(" FROM OLD_") + .append(tableName); + + String sql = sb.toString(); + db.execSQL(sql); + } + + private String getTableColumnNames(SQLiteDatabase db, String tableName) { + StringBuilder sb = new StringBuilder(); + + Cursor fields = db.rawQuery("PRAGMA table_info(" + tableName + ")", null); + while (fields.moveToNext()) { + if (!fields.isFirst()) + sb.append(", "); + sb.append(fields.getString(1)); + } + fields.close(); + + return sb.toString(); + } + + private void dropAndCreateTables(SQLiteDatabase db) { + dropAllTables(db); + onCreate(db); + } + + private void dropAllTablesWithPrefix(SQLiteDatabase db, String prefix) { + for (String indexName : mIndexNames) + db.execSQL("DROP INDEX IF EXISTS " + prefix + indexName); + for (String tableName : mTableNames) + db.execSQL("DROP TABLE IF EXISTS " + prefix + tableName); + } + + private void dropAllTables(SQLiteDatabase db) { + dropAllTablesWithPrefix(db, ""); + } +} diff --git a/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java b/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java new file mode 100644 index 0000000..2c01b30 --- /dev/null +++ b/app/src/main/java/org/connectbot/util/UberColorPickerDialog.java @@ -0,0 +1,982 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 090408 + * Keith Wiley + * kwiley@keithwiley.com + * http://keithwiley.com + * + * UberColorPickerDialog v1.1 + * + * This color picker was implemented as a (significant) extension of the + * ColorPickerDialog class provided in the Android API Demos. You are free + * to drop it unchanged into your own projects or to modify it as you see + * fit. I would appreciate it if this comment block were let intact, + * merely for credit's sake. + * + * Enjoy! + */ + +package org.connectbot.util; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ComposeShader; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.SweepGradient; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; + +/** + * UberColorPickerDialog is a seriously enhanced version of the UberColorPickerDialog + * class provided in the Android API Demos.

+ * + * NOTE (from Kenny Root): This is a VERY slimmed down version custom for ConnectBot. + * Visit Keith's site for the full version at the URL listed in the author line.

+ * + * @author Keith Wiley, kwiley@keithwiley.com, http://keithwiley.com + */ +public class UberColorPickerDialog extends Dialog { + private final OnColorChangedListener mListener; + private final int mInitialColor; + + /** + * Callback to the creator of the dialog, informing the creator of a new color and notifying that the dialog is about to dismiss. + */ + public interface OnColorChangedListener { + void colorChanged(int color); + } + + /** + * Ctor + * @param context + * @param listener + * @param initialColor + * @param showTitle If true, a title is shown across the top of the dialog. If false a toast is shown instead. + */ + public UberColorPickerDialog(Context context, + OnColorChangedListener listener, + int initialColor) { + super(context); + + mListener = listener; + mInitialColor = initialColor; + } + + /** + * Activity entry point + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + OnColorChangedListener l = new OnColorChangedListener() { + public void colorChanged(int color) { + mListener.colorChanged(color); + dismiss(); + } + }; + + DisplayMetrics dm = new DisplayMetrics(); + getWindow().getWindowManager().getDefaultDisplay().getMetrics(dm); + int screenWidth = dm.widthPixels; + int screenHeight = dm.heightPixels; + + setTitle("Pick a color (try the trackball)"); + + try { + setContentView(new ColorPickerView(getContext(), l, screenWidth, screenHeight, mInitialColor)); + } + catch (Exception e) { + //There is currently only one kind of ctor exception, that where no methods are enabled. + dismiss(); //This doesn't work! The dialog is still shown (its title at least, the layout is empty from the exception being thrown). + } + } + + /** + * ColorPickerView is the meat of this color picker (as opposed to the enclosing class). + * All the heavy lifting is done directly by this View subclass. + *

+ * You can enable/disable whichever color chooser methods you want by modifying the ENABLED_METHODS switches. They *should* + * do all the work required to properly enable/disable methods without losing track of what goes with what and what maps to what. + *

+ * If you add a new color chooser method, do a text search for "NEW_METHOD_WORK_NEEDED_HERE". That tag indicates all + * the locations in the code that will have to be amended in order to properly add a new color chooser method. + * I highly recommend adding new methods to the end of the list. If you want to try to reorder the list, you're on your own. + */ + private static class ColorPickerView extends View { + private static int SWATCH_WIDTH = 95; + private static final int SWATCH_HEIGHT = 60; + + private static int PALETTE_POS_X = 0; + private static int PALETTE_POS_Y = SWATCH_HEIGHT; + private static final int PALETTE_DIM = SWATCH_WIDTH * 2; + private static final int PALETTE_RADIUS = PALETTE_DIM / 2; + private static final int PALETTE_CENTER_X = PALETTE_RADIUS; + private static final int PALETTE_CENTER_Y = PALETTE_RADIUS; + + private static final int SLIDER_THICKNESS = 40; + + private static int VIEW_DIM_X = PALETTE_DIM; + private static int VIEW_DIM_Y = SWATCH_HEIGHT; + + //NEW_METHOD_WORK_NEEDED_HERE + private static final int METHOD_HS_V_PALETTE = 0; + + //NEW_METHOD_WORK_NEEDED_HERE + //Add a new entry to the list for each controller in the new method + private static final int TRACKED_NONE = -1; //No object on screen is currently being tracked + private static final int TRACK_SWATCH_OLD = 10; + private static final int TRACK_SWATCH_NEW = 11; + private static final int TRACK_HS_PALETTE = 30; + private static final int TRACK_VER_VALUE_SLIDER = 31; + + private static final int TEXT_SIZE = 12; + private static int[] TEXT_HSV_POS = new int[2]; + private static int[] TEXT_RGB_POS = new int[2]; + private static int[] TEXT_YUV_POS = new int[2]; + private static int[] TEXT_HEX_POS = new int[2]; + + private static final float PI = 3.141592653589793f; + + private int mMethod = METHOD_HS_V_PALETTE; + private int mTracking = TRACKED_NONE; //What object on screen is currently being tracked for movement + + //Zillions of persistant Paint objecs for drawing the View + + private Paint mSwatchOld, mSwatchNew; + + //NEW_METHOD_WORK_NEEDED_HERE + //Add Paints to represent the palettes of the new method's UI controllers + private Paint mOvalHueSat; + + private Bitmap mVerSliderBM; + private Canvas mVerSliderCv; + + private Bitmap[] mHorSlidersBM = new Bitmap[3]; + private Canvas[] mHorSlidersCv = new Canvas[3]; + + private Paint mValDimmer; + + //NEW_METHOD_WORK_NEEDED_HERE + //Add Paints to represent the icon for the new method + private Paint mOvalHueSatSmall; + + private Paint mPosMarker; + private Paint mText; + + private Rect mOldSwatchRect = new Rect(); + private Rect mNewSwatchRect = new Rect(); + private Rect mPaletteRect = new Rect(); + private Rect mVerSliderRect = new Rect(); + + private int[] mSpectrumColorsRev; + private int mOriginalColor = 0; //The color passed in at the beginning, which can be reverted to at any time by tapping the old swatch. + private float[] mHSV = new float[3]; + private int[] mRGB = new int[3]; + private float[] mYUV = new float[3]; + private String mHexStr = ""; + private boolean mHSVenabled = true; //Only true if an HSV method is enabled + private boolean mRGBenabled = true; //Only true if an RGB method is enabled + private boolean mYUVenabled = true; //Only true if a YUV method is enabled + private boolean mHexenabled = true; //Only true if an RGB method is enabled + private int[] mCoord = new int[3]; //For drawing slider/palette markers + private int mFocusedControl = -1; //Which control receives trackball events. + private OnColorChangedListener mListener; + + /** + * Ctor. + * @param c + * @param l + * @param width Used to determine orientation and adjust layout accordingly + * @param height Used to determine orientation and adjust layout accordingly + * @param color The initial color + * @throws Exception + */ + ColorPickerView(Context c, OnColorChangedListener l, int width, int height, int color) + throws Exception { + super(c); + + //We need to make the dialog focusable to retrieve trackball events. + setFocusable(true); + + mListener = l; + + mOriginalColor = color; + + Color.colorToHSV(color, mHSV); + + updateAllFromHSV(); + + //Setup the layout based on whether this is a portrait or landscape orientation. + if (width <= height) { //Portrait layout + SWATCH_WIDTH = (PALETTE_DIM + SLIDER_THICKNESS) / 2; + + PALETTE_POS_X = 0; + PALETTE_POS_Y = TEXT_SIZE * 4 + SWATCH_HEIGHT; + + //Set more rects, lots of rects + mOldSwatchRect.set(0, TEXT_SIZE * 4, SWATCH_WIDTH, TEXT_SIZE * 4 + SWATCH_HEIGHT); + mNewSwatchRect.set(SWATCH_WIDTH, TEXT_SIZE * 4, SWATCH_WIDTH * 2, TEXT_SIZE * 4 + SWATCH_HEIGHT); + mPaletteRect.set(0, PALETTE_POS_Y, PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM); + mVerSliderRect.set(PALETTE_DIM, PALETTE_POS_Y, PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM); + + TEXT_HSV_POS[0] = 3; + TEXT_HSV_POS[1] = 0; + TEXT_RGB_POS[0] = TEXT_HSV_POS[0] + 50; + TEXT_RGB_POS[1] = TEXT_HSV_POS[1]; + TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 100; + TEXT_YUV_POS[1] = TEXT_HSV_POS[1]; + TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 150; + TEXT_HEX_POS[1] = TEXT_HSV_POS[1]; + + VIEW_DIM_X = PALETTE_DIM + SLIDER_THICKNESS; + VIEW_DIM_Y = SWATCH_HEIGHT + PALETTE_DIM + TEXT_SIZE * 4; + } + else { //Landscape layout + SWATCH_WIDTH = 110; + + PALETTE_POS_X = SWATCH_WIDTH; + PALETTE_POS_Y = 0; + + //Set more rects, lots of rects + mOldSwatchRect.set(0, TEXT_SIZE * 7, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT); + mNewSwatchRect.set(0, TEXT_SIZE * 7 + SWATCH_HEIGHT, SWATCH_WIDTH, TEXT_SIZE * 7 + SWATCH_HEIGHT * 2); + mPaletteRect.set(SWATCH_WIDTH, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y + PALETTE_DIM); + mVerSliderRect.set(SWATCH_WIDTH + PALETTE_DIM, PALETTE_POS_Y, SWATCH_WIDTH + PALETTE_DIM + SLIDER_THICKNESS, PALETTE_POS_Y + PALETTE_DIM); + + TEXT_HSV_POS[0] = 3; + TEXT_HSV_POS[1] = 0; + TEXT_RGB_POS[0] = TEXT_HSV_POS[0]; + TEXT_RGB_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5); + TEXT_YUV_POS[0] = TEXT_HSV_POS[0] + 50; + TEXT_YUV_POS[1] = (int)(TEXT_HSV_POS[1] + TEXT_SIZE * 3.5); + TEXT_HEX_POS[0] = TEXT_HSV_POS[0] + 50; + TEXT_HEX_POS[1] = TEXT_HSV_POS[1]; + + VIEW_DIM_X = PALETTE_POS_X + PALETTE_DIM + SLIDER_THICKNESS; + VIEW_DIM_Y = Math.max(mNewSwatchRect.bottom, PALETTE_DIM); + } + + //Rainbows make everybody happy! + mSpectrumColorsRev = new int[] { + 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, + 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000, + }; + + //Setup all the Paint and Shader objects. There are lots of them! + + //NEW_METHOD_WORK_NEEDED_HERE + //Add Paints to represent the palettes of the new method's UI controllers + + mSwatchOld = new Paint(Paint.ANTI_ALIAS_FLAG); + mSwatchOld.setStyle(Paint.Style.FILL); + mSwatchOld.setColor(Color.HSVToColor(mHSV)); + + mSwatchNew = new Paint(Paint.ANTI_ALIAS_FLAG); + mSwatchNew.setStyle(Paint.Style.FILL); + mSwatchNew.setColor(Color.HSVToColor(mHSV)); + + Shader shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null); + Shader shaderB = new RadialGradient(0, 0, PALETTE_CENTER_X, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP); + Shader shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN); + mOvalHueSat = new Paint(Paint.ANTI_ALIAS_FLAG); + mOvalHueSat.setShader(shader); + mOvalHueSat.setStyle(Paint.Style.FILL); + mOvalHueSat.setDither(true); + + mVerSliderBM = Bitmap.createBitmap(SLIDER_THICKNESS, PALETTE_DIM, Bitmap.Config.RGB_565); + mVerSliderCv = new Canvas(mVerSliderBM); + + for (int i = 0; i < 3; i++) { + mHorSlidersBM[i] = Bitmap.createBitmap(PALETTE_DIM, SLIDER_THICKNESS, Bitmap.Config.RGB_565); + mHorSlidersCv[i] = new Canvas(mHorSlidersBM[i]); + } + + mValDimmer = new Paint(Paint.ANTI_ALIAS_FLAG); + mValDimmer.setStyle(Paint.Style.FILL); + mValDimmer.setDither(true); + mValDimmer.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); + + //Whew, we're done making the big Paints and Shaders for the swatches, palettes, and sliders. + //Now we need to make the Paints and Shaders that will draw the little method icons in the method selector list. + + //NEW_METHOD_WORK_NEEDED_HERE + //Add Paints to represent the icon for the new method + + shaderA = new SweepGradient(0, 0, mSpectrumColorsRev, null); + shaderB = new RadialGradient(0, 0, PALETTE_DIM / 2, 0xFFFFFFFF, 0xFF000000, Shader.TileMode.CLAMP); + shader = new ComposeShader(shaderA, shaderB, PorterDuff.Mode.SCREEN); + mOvalHueSatSmall = new Paint(Paint.ANTI_ALIAS_FLAG); + mOvalHueSatSmall.setShader(shader); + mOvalHueSatSmall.setStyle(Paint.Style.FILL); + + //Make a simple stroking Paint for drawing markers and borders and stuff like that. + mPosMarker = new Paint(Paint.ANTI_ALIAS_FLAG); + mPosMarker.setStyle(Paint.Style.STROKE); + mPosMarker.setStrokeWidth(2); + + //Make a basic text Paint. + mText = new Paint(Paint.ANTI_ALIAS_FLAG); + mText.setTextSize(TEXT_SIZE); + mText.setColor(Color.WHITE); + + //Kickstart + initUI(); + } + + /** + * Draw the entire view (the entire dialog). + */ + @Override + protected void onDraw(Canvas canvas) { + //Draw the old and new swatches + drawSwatches(canvas); + + //Write the text + writeColorParams(canvas); + + //Draw the palette and sliders (the UI) + if (mMethod == METHOD_HS_V_PALETTE) + drawHSV1Palette(canvas); + } + + /** + * Draw the old and new swatches. + * @param canvas + */ + private void drawSwatches(Canvas canvas) { + float[] hsv = new float[3]; + + mText.setTextSize(16); + + //Draw the original swatch + canvas.drawRect(mOldSwatchRect, mSwatchOld); + Color.colorToHSV(mOriginalColor, hsv); + //if (UberColorPickerDialog.isGray(mColor)) //Don't need this right here, but imp't to note + // hsv[1] = 0; + if (hsv[2] > .5) + mText.setColor(Color.BLACK); + canvas.drawText("Revert", mOldSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Revert") / 2, mOldSwatchRect.top + 16, mText); + mText.setColor(Color.WHITE); + + //Draw the new swatch + canvas.drawRect(mNewSwatchRect, mSwatchNew); + if (mHSV[2] > .5) + mText.setColor(Color.BLACK); + canvas.drawText("Accept", mNewSwatchRect.left + SWATCH_WIDTH / 2 - mText.measureText("Accept") / 2, mNewSwatchRect.top + 16, mText); + mText.setColor(Color.WHITE); + + mText.setTextSize(TEXT_SIZE); + } + + /** + * Write the color parametes (HSV, RGB, YUV, Hex, etc.). + * @param canvas + */ + private void writeColorParams(Canvas canvas) { + if (mHSVenabled) { + canvas.drawText("H: " + Integer.toString((int)(mHSV[0] / 360.0f * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE, mText); + canvas.drawText("S: " + Integer.toString((int)(mHSV[1] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 2, mText); + canvas.drawText("V: " + Integer.toString((int)(mHSV[2] * 255)), TEXT_HSV_POS[0], TEXT_HSV_POS[1] + TEXT_SIZE * 3, mText); + } + + if (mRGBenabled) { + canvas.drawText("R: " + mRGB[0], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE, mText); + canvas.drawText("G: " + mRGB[1], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 2, mText); + canvas.drawText("B: " + mRGB[2], TEXT_RGB_POS[0], TEXT_RGB_POS[1] + TEXT_SIZE * 3, mText); + } + + if (mYUVenabled) { + canvas.drawText("Y: " + Integer.toString((int)(mYUV[0] * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE, mText); + canvas.drawText("U: " + Integer.toString((int)((mYUV[1] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 2, mText); + canvas.drawText("V: " + Integer.toString((int)((mYUV[2] + .5f) * 255)), TEXT_YUV_POS[0], TEXT_YUV_POS[1] + TEXT_SIZE * 3, mText); + } + + if (mHexenabled) + canvas.drawText("#" + mHexStr, TEXT_HEX_POS[0], TEXT_HEX_POS[1] + TEXT_SIZE, mText); + } + + /** + * Place a small circle on the 2D palette to indicate the current values. + * @param canvas + * @param markerPosX + * @param markerPosY + */ + private void mark2DPalette(Canvas canvas, int markerPosX, int markerPosY) { + mPosMarker.setColor(Color.BLACK); + canvas.drawOval(new RectF(markerPosX - 5, markerPosY - 5, markerPosX + 5, markerPosY + 5), mPosMarker); + mPosMarker.setColor(Color.WHITE); + canvas.drawOval(new RectF(markerPosX - 3, markerPosY - 3, markerPosX + 3, markerPosY + 3), mPosMarker); + } + + /** + * Draw a line across the slider to indicate its current value. + * @param canvas + * @param markerPos + */ + private void markVerSlider(Canvas canvas, int markerPos) { + mPosMarker.setColor(Color.BLACK); + canvas.drawRect(new Rect(0, markerPos - 2, SLIDER_THICKNESS, markerPos + 3), mPosMarker); + mPosMarker.setColor(Color.WHITE); + canvas.drawRect(new Rect(0, markerPos, SLIDER_THICKNESS, markerPos + 1), mPosMarker); + } + + /** + * Frame the slider to indicate that it has trackball focus. + * @param canvas + */ + private void hilightFocusedVerSlider(Canvas canvas) { + mPosMarker.setColor(Color.WHITE); + canvas.drawRect(new Rect(0, 0, SLIDER_THICKNESS, PALETTE_DIM), mPosMarker); + mPosMarker.setColor(Color.BLACK); + canvas.drawRect(new Rect(2, 2, SLIDER_THICKNESS - 2, PALETTE_DIM - 2), mPosMarker); + } + + /** + * Frame the 2D palette to indicate that it has trackball focus. + * @param canvas + */ + private void hilightFocusedOvalPalette(Canvas canvas) { + mPosMarker.setColor(Color.WHITE); + canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mPosMarker); + mPosMarker.setColor(Color.BLACK); + canvas.drawOval(new RectF(-PALETTE_RADIUS + 2, -PALETTE_RADIUS + 2, PALETTE_RADIUS - 2, PALETTE_RADIUS - 2), mPosMarker); + } + + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate the basic draw functions here. Use the 2D palette or 1D sliders as templates for the new method. + /** + * Draw the UI for HSV with angular H and radial S combined in 2D and a 1D V slider. + * @param canvas + */ + private void drawHSV1Palette(Canvas canvas) { + canvas.save(); + + canvas.translate(PALETTE_POS_X, PALETTE_POS_Y); + + //Draw the 2D palette + canvas.translate(PALETTE_CENTER_X, PALETTE_CENTER_Y); + canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mOvalHueSat); + canvas.drawOval(new RectF(-PALETTE_RADIUS, -PALETTE_RADIUS, PALETTE_RADIUS, PALETTE_RADIUS), mValDimmer); + if (mFocusedControl == 0) + hilightFocusedOvalPalette(canvas); + mark2DPalette(canvas, mCoord[0], mCoord[1]); + canvas.translate(-PALETTE_CENTER_X, -PALETTE_CENTER_Y); + + //Draw the 1D slider + canvas.translate(PALETTE_DIM, 0); + canvas.drawBitmap(mVerSliderBM, 0, 0, null); + if (mFocusedControl == 1) + hilightFocusedVerSlider(canvas); + markVerSlider(canvas, mCoord[2]); + + canvas.restore(); + } + + /** + * Initialize the current color chooser's UI (set its color parameters and set its palette and slider values accordingly). + */ + private void initUI() { + initHSV1Palette(); + + //Focus on the first controller (arbitrary). + mFocusedControl = 0; + } + + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the last init function shown below + /** + * Initialize a color chooser. + */ + private void initHSV1Palette() { + setOvalValDimmer(); + setVerValSlider(); + + float angle = 2*PI - mHSV[0] / (180 / 3.1415927f); + float radius = mHSV[1] * PALETTE_RADIUS; + mCoord[0] = (int)(Math.cos(angle) * radius); + mCoord[1] = (int)(Math.sin(angle) * radius); + + mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM); + } + + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the set functions below, one per UI controller in the new method + /** + * Adjust a Paint which, when painted, dims its underlying object to show the effects of varying value (brightness). + */ + private void setOvalValDimmer() { + float[] hsv = new float[3]; + hsv[0] = mHSV[0]; + hsv[1] = 0; + hsv[2] = mHSV[2]; + int gray = Color.HSVToColor(hsv); + mValDimmer.setColor(gray); + } + + /** + * Create a linear gradient shader to show variations in value. + */ + private void setVerValSlider() { + float[] hsv = new float[3]; + hsv[0] = mHSV[0]; + hsv[1] = mHSV[1]; + hsv[2] = 1; + int col = Color.HSVToColor(hsv); + + int colors[] = new int[2]; + colors[0] = col; + colors[1] = 0xFF000000; + GradientDrawable gradDraw = new GradientDrawable(Orientation.TOP_BOTTOM, colors); + gradDraw.setDither(true); + gradDraw.setLevel(10000); + gradDraw.setBounds(0, 0, SLIDER_THICKNESS, PALETTE_DIM); + gradDraw.draw(mVerSliderCv); + } + + /** + * Report the correct tightly bounded dimensions of the view. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(VIEW_DIM_X, VIEW_DIM_Y); + } + + /** + * Wrap Math.round(). I'm not a Java expert. Is this the only way to avoid writing "(int)Math.round" everywhere? + * @param x + * @return + */ + private int round(double x) { + return (int)Math.round(x); + } + + /** + * Limit a value to the range [0,1]. + * @param n + * @return + */ + private float pinToUnit(float n) { + if (n < 0) { + n = 0; + } else if (n > 1) { + n = 1; + } + return n; + } + + /** + * Limit a value to the range [0,max]. + * @param n + * @param max + * @return + */ + private float pin(float n, float max) { + if (n < 0) { + n = 0; + } else if (n > max) { + n = max; + } + return n; + } + + /** + * Limit a value to the range [min,max]. + * @param n + * @param min + * @param max + * @return + */ + private float pin(float n, float min, float max) { + if (n < min) { + n = min; + } else if (n > max) { + n = max; + } + return n; + } + + /** + * No clue what this does (some sort of average/mean I presume). It came with the original UberColorPickerDialog + * in the API Demos and wasn't documented. I don't feel like spending any time figuring it out, I haven't looked at it at all. + * @param s + * @param d + * @param p + * @return + */ + private int ave(int s, int d, float p) { + return s + round(p * (d - s)); + } + + /** + * Came with the original UberColorPickerDialog in the API Demos, wasn't documented. I believe it takes an array of + * colors and a value in the range [0,1] and interpolates a resulting color in a seemingly predictable manner. + * I haven't looked at it at all. + * @param colors + * @param unit + * @return + */ + private int interpColor(int colors[], float unit) { + if (unit <= 0) { + return colors[0]; + } + if (unit >= 1) { + return colors[colors.length - 1]; + } + + float p = unit * (colors.length - 1); + int i = (int)p; + p -= i; + + // now p is just the fractional part [0...1) and i is the index + int c0 = colors[i]; + int c1 = colors[i+1]; + int a = ave(Color.alpha(c0), Color.alpha(c1), p); + int r = ave(Color.red(c0), Color.red(c1), p); + int g = ave(Color.green(c0), Color.green(c1), p); + int b = ave(Color.blue(c0), Color.blue(c1), p); + + return Color.argb(a, r, g, b); + } + + /** + * A standard point-in-rect routine. + * @param x + * @param y + * @param r + * @return true if point x,y is in rect r + */ + public boolean ptInRect(int x, int y, Rect r) { + return x > r.left && x < r.right && y > r.top && y < r.bottom; + } + + /** + * Process trackball events. Used mainly for fine-tuned color adjustment, or alternatively to switch between slider controls. + */ + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + //A longer event history implies faster trackball movement. + //Use it to infer a larger jump and therefore faster palette/slider adjustment. + int jump = event.getHistorySize() + 1; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + } + break; + case MotionEvent.ACTION_MOVE: { + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the appropriate entry in this list, + //depending on whether you use 1D or 2D controllers + switch (mMethod) { + case METHOD_HS_V_PALETTE: + if (mFocusedControl == 0) { + changeHSPalette(x, y, jump); + } + else if (mFocusedControl == 1) { + if (y < 0) + changeSlider(mFocusedControl, true, jump); + else if (y > 0) + changeSlider(mFocusedControl, false, jump); + } + break; + } + } + break; + case MotionEvent.ACTION_UP: { + } + break; + } + + return true; + } + + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the appropriate functions below, + //one per UI controller in the new method + /** + * Effect a trackball change to a 2D palette. + * @param x -1: negative x change, 0: no x change, +1: positive x change. + * @param y -1: negative y change, 0, no y change, +1: positive y change. + * @param jump the amount by which to change. + */ + private void changeHSPalette(float x, float y, int jump) { + int x2 = 0, y2 = 0; + if (x < 0) + x2 = -jump; + else if (x > 0) + x2 = jump; + if (y < 0) + y2 = -jump; + else if (y > 0) + y2 = jump; + + mCoord[0] += x2; + mCoord[1] += y2; + + if (mCoord[0] < -PALETTE_RADIUS) + mCoord[0] = -PALETTE_RADIUS; + else if (mCoord[0] > PALETTE_RADIUS) + mCoord[0] = PALETTE_RADIUS; + if (mCoord[1] < -PALETTE_RADIUS) + mCoord[1] = -PALETTE_RADIUS; + else if (mCoord[1] > PALETTE_RADIUS) + mCoord[1] = PALETTE_RADIUS; + + float radius = (float)java.lang.Math.sqrt(mCoord[0] * mCoord[0] + mCoord[1] * mCoord[1]); + if (radius > PALETTE_RADIUS) + radius = PALETTE_RADIUS; + + float angle = (float)java.lang.Math.atan2(mCoord[1], mCoord[0]); + // need to turn angle [-PI ... PI] into unit [0....1] + float unit = angle/(2*PI); + if (unit < 0) { + unit += 1; + } + + mCoord[0] = round(Math.cos(angle) * radius); + mCoord[1] = round(Math.sin(angle) * radius); + + int c = interpColor(mSpectrumColorsRev, unit); + float[] hsv = new float[3]; + Color.colorToHSV(c, hsv); + mHSV[0] = hsv[0]; + mHSV[1] = radius / PALETTE_RADIUS; + updateAllFromHSV(); + mSwatchNew.setColor(Color.HSVToColor(mHSV)); + + setVerValSlider(); + + invalidate(); + } + + /** + * Effect a trackball change to a 1D slider. + * @param slider id of the slider to be effected + * @param increase true if the change is an increase, false if a decrease + * @param jump the amount by which to change in units of the range [0,255] + */ + private void changeSlider(int slider, boolean increase, int jump) { + //NEW_METHOD_WORK_NEEDED_HERE + //It is only necessary to add an entry here for a new method if the new method uses a 1D slider. + //Note, some sliders are horizontal and others are vertical. + //They differ a bit, especially in a sign flip on the vertical axis. + if (mMethod == METHOD_HS_V_PALETTE) { + //slider *must* equal 1 + + mHSV[2] += (increase ? jump : -jump) / 256.0f; + mHSV[2] = pinToUnit(mHSV[2]); + updateAllFromHSV(); + mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM); + + mSwatchNew.setColor(Color.HSVToColor(mHSV)); + + setOvalValDimmer(); + + invalidate(); + } + } + + /** + * Keep all colorspace representations in sync. + */ + private void updateRGBfromHSV() { + int color = Color.HSVToColor(mHSV); + mRGB[0] = Color.red(color); + mRGB[1] = Color.green(color); + mRGB[2] = Color.blue(color); + } + + /** + * Keep all colorspace representations in sync. + */ + private void updateYUVfromRGB() { + float r = mRGB[0] / 255.0f; + float g = mRGB[1] / 255.0f; + float b = mRGB[2] / 255.0f; + + ColorMatrix cm = new ColorMatrix(); + cm.setRGB2YUV(); + final float[] a = cm.getArray(); + + mYUV[0] = a[0] * r + a[1] * g + a[2] * b; + mYUV[0] = pinToUnit(mYUV[0]); + mYUV[1] = a[5] * r + a[6] * g + a[7] * b; + mYUV[1] = pin(mYUV[1], -.5f, .5f); + mYUV[2] = a[10] * r + a[11] * g + a[12] * b; + mYUV[2] = pin(mYUV[2], -.5f, .5f); + } + + /** + * Keep all colorspace representations in sync. + */ + private void updateHexFromHSV() { + //For now, assume 100% opacity + mHexStr = Integer.toHexString(Color.HSVToColor(mHSV)).toUpperCase(); + mHexStr = mHexStr.substring(2, mHexStr.length()); + } + + /** + * Keep all colorspace representations in sync. + */ + private void updateAllFromHSV() { + //Update mRGB + if (mRGBenabled || mYUVenabled) + updateRGBfromHSV(); + + //Update mYUV + if (mYUVenabled) + updateYUVfromRGB(); + + //Update mHexStr + if (mRGBenabled) + updateHexFromHSV(); + } + + /** + * Process touch events: down, move, and up + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + + //Generate coordinates which are palette=local with the origin at the upper left of the main 2D palette + int y2 = (int)(pin(round(y - PALETTE_POS_Y), PALETTE_DIM)); + + //Generate coordinates which are palette-local with the origin at the center of the main 2D palette + float circlePinnedX = x - PALETTE_POS_X - PALETTE_CENTER_X; + float circlePinnedY = y - PALETTE_POS_Y - PALETTE_CENTER_Y; + + //Is the event in a swatch? + boolean inSwatchOld = ptInRect(round(x), round(y), mOldSwatchRect); + boolean inSwatchNew = ptInRect(round(x), round(y), mNewSwatchRect); + + //Get the event's distance from the center of the main 2D palette + float radius = (float)java.lang.Math.sqrt(circlePinnedX * circlePinnedX + circlePinnedY * circlePinnedY); + + //Is the event in a circle-pinned 2D palette? + boolean inOvalPalette = radius <= PALETTE_RADIUS; + + //Pin the radius + if (radius > PALETTE_RADIUS) + radius = PALETTE_RADIUS; + + //Is the event in a vertical slider to the right of the main 2D palette + boolean inVerSlider = ptInRect(round(x), round(y), mVerSliderRect); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mTracking = TRACKED_NONE; + + if (inSwatchOld) + mTracking = TRACK_SWATCH_OLD; + else if (inSwatchNew) + mTracking = TRACK_SWATCH_NEW; + + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the last entry in this list + else if (mMethod == METHOD_HS_V_PALETTE) { + if (inOvalPalette) { + mTracking = TRACK_HS_PALETTE; + mFocusedControl = 0; + } + else if (inVerSlider) { + mTracking = TRACK_VER_VALUE_SLIDER; + mFocusedControl = 1; + } + } + case MotionEvent.ACTION_MOVE: + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the entries in this list, + //one per UI controller the new method requires. + if (mTracking == TRACK_HS_PALETTE) { + float angle = (float)java.lang.Math.atan2(circlePinnedY, circlePinnedX); + // need to turn angle [-PI ... PI] into unit [0....1] + float unit = angle/(2*PI); + if (unit < 0) { + unit += 1; + } + + mCoord[0] = round(Math.cos(angle) * radius); + mCoord[1] = round(Math.sin(angle) * radius); + + int c = interpColor(mSpectrumColorsRev, unit); + float[] hsv = new float[3]; + Color.colorToHSV(c, hsv); + mHSV[0] = hsv[0]; + mHSV[1] = radius / PALETTE_RADIUS; + updateAllFromHSV(); + mSwatchNew.setColor(Color.HSVToColor(mHSV)); + + setVerValSlider(); + + invalidate(); + } + else if (mTracking == TRACK_VER_VALUE_SLIDER) { + if (mCoord[2] != y2) { + mCoord[2] = y2; + float value = 1.0f - (float)y2 / (float)PALETTE_DIM; + + mHSV[2] = value; + updateAllFromHSV(); + mSwatchNew.setColor(Color.HSVToColor(mHSV)); + + setOvalValDimmer(); + + invalidate(); + } + } + break; + case MotionEvent.ACTION_UP: + //NEW_METHOD_WORK_NEEDED_HERE + //To add a new method, replicate and extend the last entry in this list. + if (mTracking == TRACK_SWATCH_OLD && inSwatchOld) { + Color.colorToHSV(mOriginalColor, mHSV); + mSwatchNew.setColor(mOriginalColor); + initUI(); + invalidate(); + } + else if (mTracking == TRACK_SWATCH_NEW && inSwatchNew) { + mListener.colorChanged(mSwatchNew.getColor()); + invalidate(); + } + + mTracking= TRACKED_NONE; + break; + } + + return true; + } + } +} diff --git a/app/src/main/java/org/connectbot/util/VolumePreference.java b/app/src/main/java/org/connectbot/util/VolumePreference.java new file mode 100644 index 0000000..2e7f61c --- /dev/null +++ b/app/src/main/java/org/connectbot/util/VolumePreference.java @@ -0,0 +1,72 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import android.content.Context; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +/** + * @author kenny + * + */ +public class VolumePreference extends DialogPreference implements OnSeekBarChangeListener { + /** + * @param context + * @param attrs + */ + public VolumePreference(Context context, AttributeSet attrs) { + super(context, attrs); + + setupLayout(context, attrs); + } + + public VolumePreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setupLayout(context, attrs); + } + + private void setupLayout(Context context, AttributeSet attrs) { + setPersistent(true); + } + + @Override + protected View onCreateDialogView() { + SeekBar sb = new SeekBar(getContext()); + + sb.setMax(100); + sb.setProgress((int)(getPersistedFloat( + PreferenceConstants.DEFAULT_BELL_VOLUME) * 100)); + sb.setPadding(10, 10, 10, 10); + sb.setOnSeekBarChangeListener(this); + + return sb; + } + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + persistFloat(progress / 100f); + } + + public void onStartTrackingTouch(SeekBar seekBar) { } + + public void onStopTrackingTouch(SeekBar seekBar) { } +} diff --git a/app/src/main/java/org/connectbot/util/XmlBuilder.java b/app/src/main/java/org/connectbot/util/XmlBuilder.java new file mode 100644 index 0000000..4a6f62d --- /dev/null +++ b/app/src/main/java/org/connectbot/util/XmlBuilder.java @@ -0,0 +1,71 @@ +/* + * ConnectBot: simple, powerful, open-source SSH client for Android + * Copyright 2007 Kenny Root, Jeffrey Sharkey + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.connectbot.util; + +import com.trilead.ssh2.crypto.Base64; + +/** + * @author Kenny Root + * + */ +public class XmlBuilder { + private StringBuilder sb; + + public XmlBuilder() { + sb = new StringBuilder(); + } + + public XmlBuilder append(String data) { + sb.append(data); + + return this; + } + + public XmlBuilder append(String field, Object data) { + if (data == null) { + sb.append(String.format("<%s/>", field)); + } else if (data instanceof String) { + String input = (String) data; + boolean binary = false; + + for (byte b : input.getBytes()) { + if (b < 0x20 || b > 0x7e) { + binary = true; + break; + } + } + + sb.append(String.format("<%s>%s", field, + binary ? new String(Base64.encode(input.getBytes())) : input, field)); + } else if (data instanceof Integer) { + sb.append(String.format("<%s>%d", field, (Integer) data, field)); + } else if (data instanceof Long) { + sb.append(String.format("<%s>%d", field, (Long) data, field)); + } else if (data instanceof byte[]) { + sb.append(String.format("<%s>%s", field, new String(Base64.encode((byte[]) data)), field)); + } else if (data instanceof Boolean) { + sb.append(String.format("<%s>%s", field, (Boolean) data, field)); + } + + return this; + } + + public String toString() { + return sb.toString(); + } +} diff --git a/app/src/main/java/org/keyczar/jce/EcCore.java b/app/src/main/java/org/keyczar/jce/EcCore.java new file mode 100644 index 0000000..681d5db --- /dev/null +++ b/app/src/main/java/org/keyczar/jce/EcCore.java @@ -0,0 +1,679 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.keyczar.jce; + +import java.math.BigInteger; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; + +/** + * This class implements the basic EC operations such as point addition and + * doubling and point multiplication. Only NSA Suite B / NIST curves are + * supported. + * + * Todo: + * - Add (more) comments - Performance optimizations - Cleanup ASN.1 code, + * possibly replace with own impl - ... + * + * References: + * + * [1] Software Implementation of the NIST Elliptic Curves Over Prime Fields, M. + * Brown et al. [2] Efficient elliptic curve exponentiation using mixed + * coordinates, H. Cohen et al. [3] SEC 1: Elliptic Curve Cryptography. [4] + * Guide to Elliptic Curve Cryptography, D. Hankerson et al., Springer. + * + * @author martclau@gmail.com + * + */ +// BEGIN connectbot-changed +public final class EcCore { +// END connectbot-changed +// BEGIN connectbot-removed +// private static final long serialVersionUID = -1376116429660095993L; +// +// private static final String INFO = "Google Keyczar (EC key/parameter generation; EC signing)"; +// +// public static final String NAME = "GooKey"; +// +// @SuppressWarnings("unchecked") +// public EcCore() { +// super(NAME, 0.1, INFO); +// AccessController.doPrivileged(new PrivilegedAction() { +// @Override +// public Object run() { +// put("Signature.SHA1withECDSA", "org.keyczar.jce.EcSignatureImpl$SHA1"); +// put("Alg.Alias.Signature.ECDSA", "SHA1withDSA"); +// put("Signature.SHA256withECDSA", +// "org.keyczar.jce.EcSignatureImpl$SHA256"); +// put("Signature.SHA384withECDSA", +// "org.keyczar.jce.EcSignatureImpl$SHA384"); +// put("Signature.SHA512withECDSA", +// "org.keyczar.jce.EcSignatureImpl$SHA512"); +// put("KeyPairGenerator.EC", "org.keyczar.jce.EcKeyPairGeneratorImpl"); +// put("KeyFactory.EC", "org.keyczar.jce.EcKeyFactoryImpl"); +// put("Signature.SHA1withECDSA KeySize", "521"); +// put("Signature.SHA1withECDSA ImplementedIn", "Software"); +// put("Signature.SHA256withECDSA KeySize", "521"); +// put("Signature.SHA256withECDSA ImplementedIn", "Software"); +// put("Signature.SHA384withECDSA KeySize", "521"); +// put("Signature.SHA384withECDSA ImplementedIn", "Software"); +// put("Signature.SHA512withECDSA KeySize", "521"); +// put("Signature.SHA512withECDSA ImplementedIn", "Software"); +// put("KeyPairGenerator.EC ImplementedIn", "Software"); +// put("KeyFactory.EC ImplementedIn", "Software"); +// return null; +// } +// }); +// } +// +// private static final ECParameterSpec P192 = new ECParameterSpec( +// new EllipticCurve( +// new ECFieldFp(new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF", 16)), +// new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC", 16), +// new BigInteger("64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1", 16)), +// new ECPoint( +// new BigInteger("188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012", 16), +// new BigInteger("07192B95FFC8DA78631011ED6B24CDD573F977A11E794811", 16)), +// new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831", 16), 1); +// +// private static final ECParameterSpec P224 = new ECParameterSpec( +// new EllipticCurve(new ECFieldFp(new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001", 16)), +// new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE", 16), +// new BigInteger( +// "B4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4", 16)), +// new ECPoint(new BigInteger( +// "B70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21", 16), +// new BigInteger( +// "BD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34", 16)), +// new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D", 16), 1); +// +// private static final ECParameterSpec P256 = new ECParameterSpec( +// new EllipticCurve(new ECFieldFp(new BigInteger( +// "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", +// 16)), new BigInteger( +// "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", +// 16), new BigInteger( +// "5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", +// 16)), new ECPoint(new BigInteger( +// "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", +// 16), new BigInteger( +// "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", +// 16)), new BigInteger( +// "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", +// 16), 1); +// +// private static final ECParameterSpec P384 = new ECParameterSpec( +// new EllipticCurve( +// new ECFieldFp( +// new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", +// 16)), +// new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", +// 16), +// new BigInteger( +// "B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", +// 16)), +// new ECPoint( +// new BigInteger( +// "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", +// 16), +// new BigInteger( +// "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", +// 16)), +// new BigInteger( +// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", +// 16), 1); +// +// private static final ECParameterSpec P521 = new ECParameterSpec( +// new EllipticCurve( +// new ECFieldFp( +// new BigInteger( +// "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", +// 16)), +// new BigInteger( +// "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC", +// 16), +// new BigInteger( +// "0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00", +// 16)), +// new ECPoint( +// new BigInteger( +// "00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66", +// 16), +// new BigInteger( +// "011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650", +// 16)), +// new BigInteger( +// "01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", +// 16), 1); +// +// public static final String EC_PARAMS_P192_OID = "1.2.840.10045.3.1.1"; +// public static final String EC_PARAMS_P224_OID = "1.3.132.0.33"; +// public static final String EC_PARAMS_P256_OID = "1.2.840.10045.3.1.7"; +// public static final String EC_PARAMS_P384_OID = "1.3.132.0.34"; +// public static final String EC_PARAMS_P521_OID = "1.3.132.0.35"; +// +// private static Map oidMap = new HashMap(); +// private static Map paramsMap = new HashMap(); +// private static Map friendlyNameMap = new HashMap(); +// +// static { +// oidMap.put(EC_PARAMS_P192_OID, P192); +// oidMap.put(EC_PARAMS_P224_OID, P224); +// oidMap.put(EC_PARAMS_P256_OID, P256); +// oidMap.put(EC_PARAMS_P384_OID, P384); +// oidMap.put(EC_PARAMS_P521_OID, P521); +// paramsMap.put(P192, EC_PARAMS_P192_OID); +// paramsMap.put(P224, EC_PARAMS_P224_OID); +// paramsMap.put(P256, EC_PARAMS_P256_OID); +// paramsMap.put(P384, EC_PARAMS_P384_OID); +// paramsMap.put(P521, EC_PARAMS_P521_OID); +// friendlyNameMap.put(P192, "P-192"); +// friendlyNameMap.put(P224, "P-224"); +// friendlyNameMap.put(P256, "P-256"); +// friendlyNameMap.put(P384, "P-384"); +// friendlyNameMap.put(P521, "P-521"); +// } +// +// public static ECParameterSpec getParams(String oid) { +// ECParameterSpec params; +// if ((params = oidMap.get(oid)) != null) return params; +// throw new IllegalArgumentException("Unsupported EC parameters: " + oid); +// } +// +// public static String getOID(ECParameterSpec params) { +// String oid; +// if ((oid = paramsMap.get(params)) != null) return oid; +// throw new IllegalArgumentException("Unsupport EC parameters"); +// } +// +// public static String getFriendlyName(ECParameterSpec params) { +// String name; +// if ((name = friendlyNameMap.get(params)) != null) return name; +// throw new IllegalArgumentException("Unsupport EC parameters"); +// } +// +// private static final BigInteger ZERO = BigInteger.ZERO; +// private static final BigInteger ONE = BigInteger.ONE; +// private static final BigInteger TWO = BigInteger.valueOf(2); +// END connectbot-removed + private static final BigInteger THREE = BigInteger.valueOf(3); +// BEGIN connectbot-removed +// private static final BigInteger FOUR = BigInteger.valueOf(4); +// private static final BigInteger EIGHT = BigInteger.valueOf(8); +// END connectbot-removed + + private static BigInteger[] doublePointA(BigInteger[] P, + ECParameterSpec params) { + final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); + final BigInteger a = params.getCurve().getA(); + + if (P[0] == null || P[1] == null) return P; + + BigInteger d = (P[0].pow(2).multiply(THREE).add(a)).multiply(P[1] + .shiftLeft(1).modInverse(p)); + BigInteger[] R = new BigInteger[2]; + R[0] = d.pow(2).subtract(P[0].shiftLeft(1)).mod(p); + R[1] = d.multiply(P[0].subtract(R[0])).subtract(P[1]).mod(p); + + return R; + } + + private static BigInteger[] addPointsA(BigInteger[] P1, BigInteger[] P2, + ECParameterSpec params) { + final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); + + if (P2[0] == null || P2[1] == null) return P1; + + if (P1[0] == null || P1[1] == null) return P2; + + BigInteger d = (P2[1].subtract(P1[1])).multiply((P2[0].subtract(P1[0])) + .modInverse(p)); + BigInteger[] R = new BigInteger[2]; + R[0] = d.pow(2).subtract(P1[0]).subtract(P2[0]).mod(p); + R[1] = d.multiply(P1[0].subtract(R[0])).subtract(P1[1]).mod(p); + + return R; + } + + public static BigInteger[] multiplyPointA(BigInteger[] P, BigInteger k, + ECParameterSpec params) { + BigInteger[] Q = new BigInteger[] {null, null}; + + for (int i = k.bitLength() - 1; i >= 0; i--) { + Q = doublePointA(Q, params); + if (k.testBit(i)) Q = addPointsA(Q, P, params); + } + + return Q; + } + +// BEGIN connectbot-removed +// private static BigInteger[] doublePointJ(BigInteger[] P, +// ECParameterSpec params) { +// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); +// BigInteger A, B, C, D; +// +// if (P[2].signum() == 0) // point at inf +// return P; +// +// A = FOUR.multiply(P[0]).multiply(P[1].pow(2)).mod(p); +// B = EIGHT.multiply(P[1].pow(4)).mod(p); +// C = THREE.multiply(P[0].subtract(P[2].pow(2))).multiply( +// P[0].add(P[2].pow(2))).mod(p); +// D = C.pow(2).subtract(A.add(A)).mod(p); +// +// return new BigInteger[] { +// D, C.multiply(A.subtract(D)).subtract(B).mod(p), +// TWO.multiply(P[1]).multiply(P[2]).mod(p)}; +// } +// +// private static BigInteger[] addPointsJA(BigInteger[] P1, BigInteger[] P2, +// ECParameterSpec params) { +// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); +// BigInteger A, B, C, D; +// BigInteger X3; +// +// if (P1[2].signum() == 0) // point at inf +// return new BigInteger[] {P2[0], P2[1], ONE}; +// +// A = P2[0].multiply(P1[2].pow(2)).mod(p); +// B = P2[1].multiply(P1[2].pow(3)).mod(p); +// C = A.subtract(P1[0]).mod(p); +// D = B.subtract(P1[1]).mod(p); +// +// X3 = D.pow(2) +// .subtract(C.pow(3).add(TWO.multiply(P1[0]).multiply(C.pow(2)))).mod(p); +// return new BigInteger[] { +// X3, +// D.multiply(P1[0].multiply(C.pow(2)).subtract(X3)).subtract( +// P1[1].multiply(C.pow(3))).mod(p), P1[2].multiply(C).mod(p)}; +// } +// +// // Binary NAF method for point multiplication +// public static BigInteger[] multiplyPoint(BigInteger[] P, BigInteger k, +// ECParameterSpec params) { +// BigInteger h = THREE.multiply(k); +// +// BigInteger[] Pneg = new BigInteger[] {P[0], P[1].negate()}; +// BigInteger[] R = new BigInteger[] {P[0], P[1], ONE}; +// +// int bitLen = h.bitLength(); +// for (int i = bitLen - 2; i > 0; --i) { +// R = doublePointJ(R, params); +// if (h.testBit(i)) R = addPointsJA(R, P, params); +// if (k.testBit(i)) R = addPointsJA(R, Pneg, params); +// } +// +// // // +// // BigInteger[] SS = new BigInteger[] { R[0], R[1], R[2] }; +// // toAffine(SS, params); +// // BigInteger[] RR = multiplyPointA(P, k, params); +// // if (!SS[0].equals(RR[0]) || !SS[1].equals(RR[1])) +// // throw new RuntimeException("Internal mult error"); +// // // +// +// return R; +// } + +// // Simultaneous multiple point multiplication, also known as Shamir's trick +// static BigInteger[] multiplyPoints(BigInteger[] P, BigInteger k, +// BigInteger[] Q, BigInteger l, ECParameterSpec params) { +// BigInteger[] PQ = addPointsA(P, Q, params); +// BigInteger[] R = new BigInteger[] {null, null, ZERO}; +// +// int max = Math.max(k.bitLength(), l.bitLength()); +// for (int i = max - 1; i >= 0; --i) { +// R = doublePointJ(R, params); +// if (k.testBit(i)) { +// if (l.testBit(i)) +// R = addPointsJA(R, PQ, params); +// else +// R = addPointsJA(R, P, params); +// } else if (l.testBit(i)) R = addPointsJA(R, Q, params); +// } +// +// // // +// // BigInteger[] SS = new BigInteger[] { R[0], R[1], R[2] }; +// // toAffine(SS, params); +// // BigInteger[] AA = multiplyPointA(P, k, params); +// // BigInteger[] BB = multiplyPointA(Q, l, params); +// // BigInteger[] AB = addPointsA(AA, BB, params); +// // if (!SS[0].equals(AB[0]) || !SS[1].equals(AB[1])) +// // throw new RuntimeException("Internal mult error"); +// // // +// +// return R; +// } +// +// // SEC 1, 2.3.5 +// static byte[] fieldElemToBytes(BigInteger a, ECParameterSpec params) { +// int len = (((ECFieldFp) params.getCurve().getField()).getP().bitLength() + 7) / 8; +// byte[] bytes = a.toByteArray(); +// if (len < bytes.length) { +// byte[] tmp = new byte[len]; +// System.arraycopy(bytes, bytes.length - tmp.length, tmp, 0, tmp.length); +// return tmp; +// } else if (len > bytes.length) { +// byte[] tmp = new byte[len]; +// System.arraycopy(bytes, 0, tmp, tmp.length - bytes.length, bytes.length); +// return tmp; +// } +// return bytes; +// } +// +// static int fieldElemToBytes(BigInteger a, ECParameterSpec params, +// byte[] data, int off) { +// int len = (((ECFieldFp) params.getCurve().getField()).getP().bitLength() + 7) / 8; +// byte[] bytes = a.toByteArray(); +// if (len < bytes.length) { +// System.arraycopy(bytes, bytes.length - len, data, off, len); +// return len; +// } else if (len > bytes.length) { +// System.arraycopy(bytes, 0, data, len - bytes.length + off, bytes.length); +// return len; +// } +// System.arraycopy(bytes, 0, data, off, bytes.length); +// return bytes.length; +// } +// +// // SEC 1, 2.3.3 +// static byte[] ecPointToBytes(ECPoint a, ECParameterSpec params) { +// byte[] fe1 = fieldElemToBytes(a.getAffineX(), params); +// byte[] fe2 = fieldElemToBytes(a.getAffineY(), params); +// byte[] bytes = new byte[1 + fe1.length + fe2.length]; +// bytes[0] = 0x04; +// System.arraycopy(fe1, 0, bytes, 1, fe1.length); +// System.arraycopy(fe2, 0, bytes, 1 + fe1.length, fe2.length); +// return bytes; +// } +// +// // SEC 1, 2.3.4 +// static ECPoint bytesToECPoint(byte[] bytes, ECParameterSpec params) { +// switch (bytes[0]) { +// case 0x00: // point at inf +// throw new IllegalArgumentException( +// "Point at infinity is not a valid argument"); +// case 0x02: // point compression +// case 0x03: +// throw new UnsupportedOperationException( +// "Point compression is not supported"); +// case 0x04: +// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); +// byte[] fe = new byte[(p.bitLength() + 7) / 8]; +// System.arraycopy(bytes, 1, fe, 0, fe.length); +// BigInteger x = new BigInteger(1, fe); +// System.arraycopy(bytes, 1 + fe.length, fe, 0, fe.length); +// return new ECPoint(x, new BigInteger(1, fe)); +// default: +// throw new IllegalArgumentException("Invalid point encoding"); +// } +// } +// +// // Convert Jacobian point to affine +// static void toAffine(BigInteger[] P, ECParameterSpec params) { +// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); +// P[0] = P[0].multiply(P[2].pow(2).modInverse(p)).mod(p); +// P[1] = P[1].multiply(P[2].pow(3).modInverse(p)).mod(p); +// } +// +// static void toAffineX(BigInteger[] P, ECParameterSpec params) { +// final BigInteger p = ((ECFieldFp) params.getCurve().getField()).getP(); +// P[0] = P[0].multiply(P[2].pow(2).modInverse(p)).mod(p); +// } +// +// static BigInteger[] internalPoint(ECPoint P) { +// return new BigInteger[] {P.getAffineX(), P.getAffineY()}; +// } +// +// // private static void printPerf(String msg, long start, long stop) { +// // String unit = "ms"; +// // long diff = stop - start; +// // if (diff > 1000) { +// // diff /= 1000; +// // unit = "s"; +// // } +// // System.out.printf("%s: %d %s\n", msg, diff, unit); +// // } +// +// public static void main(String[] args) throws Exception { +// +// Security.insertProviderAt(new EcCore(), 0); +// +// // ---- +// // Test primitives +// // ---- +// +// // GooKey EC private key, 256 bit +// // Private value: +// // a9231e0d113abdacd3bb5edb24124fbef6f562c5f90b835670f5e48f775019f2 +// // Parameters: P-256 (1.2.840.10045.3.1.7) +// // GooKey EC public key, 256 bit +// // Public value (x coordinate): +// // 86645e0320c0f9dc1a9b8456396cc105754df67a9829c21e13ab6ecf944cf68c +// // Public value (y coordinate): +// // ea1721a578043d48f12738359b5eb5f0dac2242ec6128ee0ab6ff40c8fe0cae6 +// // Parameters: P-256 (1.2.840.10045.3.1.7) +// // GooKey EC private key, 256 bit +// // Private value: +// // b84d5cfab214fc3928864abb85f668a85b1006ca0147c78f22deb1dcc7e4a022 +// // Parameters: P-256 (1.2.840.10045.3.1.7) +// // GooKey EC public key, 256 bit +// // Public value (x coordinate): +// // 61f6f7264f0a19f0debcca3efd079667a0112cc0b8be07a815b4c375e96ad3d1 +// // Public value (y coordinate): +// // 3308c0016d776ed5aa9f021e43348b2e684b3b7a0f25dc9e4c8670b5d87cb705 +// // Parameters: P-256 (1.2.840.10045.3.1.7) +// +// // P = kG +// BigInteger k = new BigInteger( +// "a9231e0d113abdacd3bb5edb24124fbef6f562c5f90b835670f5e48f775019f2", 16); +// BigInteger[] P = new BigInteger[] { +// new BigInteger( +// "86645e0320c0f9dc1a9b8456396cc105754df67a9829c21e13ab6ecf944cf68c", +// 16), +// new BigInteger( +// "ea1721a578043d48f12738359b5eb5f0dac2242ec6128ee0ab6ff40c8fe0cae6", +// 16), ONE}; +// +// // Q = lG +// BigInteger l = new BigInteger( +// "b84d5cfab214fc3928864abb85f668a85b1006ca0147c78f22deb1dcc7e4a022", 16); +// BigInteger[] Q = new BigInteger[] { +// new BigInteger( +// "61f6f7264f0a19f0debcca3efd079667a0112cc0b8be07a815b4c375e96ad3d1", +// 16), +// new BigInteger( +// "3308c0016d776ed5aa9f021e43348b2e684b3b7a0f25dc9e4c8670b5d87cb705", +// 16), ONE}; +// +// // Known answer for P+Q +// BigInteger[] kat1 = new BigInteger[] { +// new BigInteger( +// "bc7adb05bca2460bbfeb4e0f88b61c384ea88ed3fd56017938ac2582513d4220", +// 16), +// new BigInteger( +// "a640a43df2e9df39eec11445b7e3f7835b743ef1ac4a83cecb570a060b3f1c6c", +// 16)}; +// +// BigInteger[] R = addPointsA(P, Q, P256); +// if (!R[0].equals(kat1[0]) || !R[1].equals(kat1[1])) +// throw new RuntimeException("kat1 failed"); +// +// R = addPointsJA(P, Q, P256); +// toAffine(R, P256); +// if (!R[0].equals(kat1[0]) || !R[1].equals(kat1[1])) +// throw new RuntimeException("kat1 failed"); +// +// +// // Known answer for Q+Q +// BigInteger[] kat2 = new BigInteger[] { +// new BigInteger( +// "c79d7f9100c14a70f0bb9bdce59654abf99e10d1ac5afc1a0f1b6bc650d6429b", +// 16), +// new BigInteger( +// "6856814e47adce42bc0d7c3bef308c6c737c418ed093effb31e21f53c7735c97", +// 16)}; +// +// R = doublePointA(P, P256); +// if (!R[0].equals(kat2[0]) || !R[1].equals(kat2[1])) +// throw new RuntimeException("kat2 failed"); +// +// R = doublePointJ(P, P256); +// toAffine(R, P256); +// if (!R[0].equals(kat2[0]) || !R[1].equals(kat2[1])) +// throw new RuntimeException("kat2 failed"); +// +// // Known answer for kP +// BigInteger[] kat3 = new BigInteger[] { +// new BigInteger( +// "97a82a834b9e6b50660ae30d43dac9b200276e8bcd2ed6a6593048de09276d1a", +// 16), +// new BigInteger( +// "30a9590a01066d8ef54a910afcc8648dbc7400c01750af423ce95547f2154d56", +// 16)}; +// +// R = multiplyPointA(P, k, P256); +// if (!R[0].equals(kat3[0]) || !R[1].equals(kat3[1])) +// throw new RuntimeException("kat3 failed"); +// +// R = multiplyPoint(P, k, P256); +// toAffine(R, P256); +// if (!R[0].equals(kat3[0]) || !R[1].equals(kat3[1])) +// throw new RuntimeException("kat3 failed"); +// +// // Known answer for kP+lQ +// BigInteger[] kat4 = new BigInteger[] { +// new BigInteger( +// "6fd51be5cf3d6a6bcb62594bbe41ccf549b37d8fefff6e293a5bea0836efcfc6", +// 16), +// new BigInteger( +// "9bc21a930137aa3814908974c431e4545a05dce61321253c337f3883129c42ca", +// 16)}; +// +// BigInteger[] RR = multiplyPointA(Q, l, P256); +// R = addPointsA(R, RR, P256); +// if (!R[0].equals(kat4[0]) || !R[1].equals(kat4[1])) +// throw new RuntimeException("kat4 failed"); +// +// R = multiplyPoints(P, k, Q, l, P256); +// toAffine(R, P256); +// if (!R[0].equals(kat4[0]) || !R[1].equals(kat4[1])) +// throw new RuntimeException("kat4 failed"); +// +// // ---- +// // Test ECDSA in various combinations +// // ---- +// +// Provider gooProv = Security.getProvider("GooKey"); +// Provider nssProv = Security.getProvider("SunPKCS11-NSS"); +// +// // Number of iterations: trust me, this is a (stress) good test +// // and does provoke bugs in a fuzzing way. +// int iter = 50; +// +// // Iterate over all key lengths and signature schemes. +// int[] keyLengths = {192, 224, 256, 384, 521}; +// String[] ecdsas = { +// "SHA1withECDSA", "SHA256withECDSA", "SHA384withECDSA", +// "SHA512withECDSA"}; +// for (int s = 0; s < ecdsas.length; s++) { +// System.out.println("Signature scheme " + ecdsas[s]); +// for (int i = 0; i < keyLengths.length; i++) { +// System.out.print("Testing P-" + keyLengths[i] + ": "); +// for (int n = 0; n < iter; n++) { +// System.out.print("."); +// +// KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", gooProv); +// kpGen.initialize(keyLengths[i]); +// KeyPair ecKeyPair = kpGen.generateKeyPair(); +// +// ECPrivateKey ecPrivKey = (ECPrivateKey) ecKeyPair.getPrivate(); +// byte[] tmp = ecPrivKey.getEncoded(); +// KeyFactory keyFab = KeyFactory.getInstance("EC", gooProv); +// keyFab.generatePrivate(new PKCS8EncodedKeySpec(tmp)); +// ECPrivateKeySpec ecPrivSpec = new ECPrivateKeySpec(ecPrivKey.getS(), +// ecPrivKey.getParams()); +// keyFab.generatePrivate(ecPrivSpec); +// +// ECPublicKey ecPubKey = (ECPublicKey) ecKeyPair.getPublic(); +// tmp = ecPubKey.getEncoded(); // dont modify tmp now - is used below +// keyFab.generatePublic(new X509EncodedKeySpec(tmp)); +// ECPublicKeySpec ecPubSpec = new ECPublicKeySpec(ecPubKey.getW(), +// ecPubKey.getParams()); +// keyFab.generatePublic(ecPubSpec); +// +// Signature ecdsa = Signature.getInstance(ecdsas[s], gooProv); +// ecdsa.initSign(ecPrivKey); +// ecdsa.update(tmp); +// byte[] sig = ecdsa.sign(); +// ecdsa.initVerify(ecPubKey); +// ecdsa.update(tmp); +// if (!ecdsa.verify(sig)) +// throw new RuntimeException("Signature not verified: " +// + keyLengths[i]); +// +// // Cross verify using NSS if present +// if (nssProv != null) { +// keyFab = KeyFactory.getInstance("EC", nssProv); +// +// // For some reason NSS doesnt seem to work for P-192 and P-224?! +// if (keyLengths[i] == 192 || keyLengths[i] == 224) continue; +// +// ECPrivateKey nssPrivKey = (ECPrivateKey) keyFab +// .generatePrivate(new PKCS8EncodedKeySpec(ecPrivKey.getEncoded())); +// ECPublicKey nssPubKey = (ECPublicKey) keyFab +// .generatePublic(new X509EncodedKeySpec(ecPubKey.getEncoded())); +// +// ecdsa = Signature.getInstance(ecdsas[s], nssProv); +// ecdsa.initVerify(nssPubKey); +// ecdsa.update(tmp); +// if (!ecdsa.verify(sig)) +// throw new RuntimeException("Signature not verified 2: " +// + keyLengths[i]); +// +// ecdsa.initSign(nssPrivKey); +// ecdsa.update(tmp); +// sig = ecdsa.sign(); +// ecdsa = Signature.getInstance(ecdsas[s], gooProv); +// ecdsa.initVerify(ecPubKey); +// ecdsa.update(tmp); +// if (!ecdsa.verify(sig)) +// throw new RuntimeException("Signature not verified 3: " +// + keyLengths[i]); +// } +// } +// System.out.println(" done"); +// } +// } +// +// // Test Keyczar integration +// // Signer ecdsaSigner = new Signer("c:\\temp\\eckeyset"); +// // String tbs = "Sign this"; +// // String sig = ecdsaSigner.sign(tbs); +// // if (ecdsaSigner.verify(sig, tbs)) +// // System.out.println("Keyczar EC OK"); +// // else +// // System.out.println("Keyczar EC not OK"); +// } +//END connectbot-removed +} diff --git a/app/src/main/java/org/openintents/intents/FileManagerIntents.java b/app/src/main/java/org/openintents/intents/FileManagerIntents.java new file mode 100644 index 0000000..fba2555 --- /dev/null +++ b/app/src/main/java/org/openintents/intents/FileManagerIntents.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2008 OpenIntents.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openintents.intents; + +// Version Dec 9, 2008 + + +/** + * Provides OpenIntents actions, extras, and categories used by providers. + *

These specifiers extend the standard Android specifiers.

+ */ +public final class FileManagerIntents { + + /** + * Activity Action: Pick a file through the file manager, or let user + * specify a custom file name. + * Data is the current file name or file name suggestion. + * Returns a new file name as file URI in data. + * + *

Constant Value: "org.openintents.action.PICK_FILE"

+ */ + public static final String ACTION_PICK_FILE = "org.openintents.action.PICK_FILE"; + + /** + * Activity Action: Pick a directory through the file manager, or let user + * specify a custom file name. + * Data is the current directory name or directory name suggestion. + * Returns a new directory name as file URI in data. + * + *

Constant Value: "org.openintents.action.PICK_DIRECTORY"

+ */ + public static final String ACTION_PICK_DIRECTORY = "org.openintents.action.PICK_DIRECTORY"; + + /** + * The title to display. + * + *

This is shown in the title bar of the file manager.

+ * + *

Constant Value: "org.openintents.extra.TITLE"

+ */ + public static final String EXTRA_TITLE = "org.openintents.extra.TITLE"; + + /** + * The text on the button to display. + * + *

Depending on the use, it makes sense to set this to "Open" or "Save".

+ * + *

Constant Value: "org.openintents.extra.BUTTON_TEXT"

+ */ + public static final String EXTRA_BUTTON_TEXT = "org.openintents.extra.BUTTON_TEXT"; + +} diff --git a/app/src/main/jni/com_google_ase_Exec.cpp b/app/src/main/jni/com_google_ase_Exec.cpp new file mode 100644 index 0000000..b78f356 --- /dev/null +++ b/app/src/main/jni/com_google_ase_Exec.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "com_google_ase_Exec.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "android/log.h" + +#define LOG_TAG "Exec" +#define LOG(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +void JNU_ThrowByName(JNIEnv* env, const char* name, const char* msg) { + jclass clazz = env->FindClass(name); + if (clazz != NULL) { + env->ThrowNew(clazz, msg); + } + env->DeleteLocalRef(clazz); +} + +char* JNU_GetStringNativeChars(JNIEnv* env, jstring jstr) { + if (jstr == NULL) { + return NULL; + } + jbyteArray bytes = 0; + jthrowable exc; + char* result = 0; + if (env->EnsureLocalCapacity(2) < 0) { + return 0; /* out of memory error */ + } + jclass Class_java_lang_String = env->FindClass("java/lang/String"); + jmethodID MID_String_getBytes = env->GetMethodID( + Class_java_lang_String, "getBytes", "()[B"); + bytes = (jbyteArray) env->CallObjectMethod(jstr, MID_String_getBytes); + exc = env->ExceptionOccurred(); + if (!exc) { + jint len = env->GetArrayLength(bytes); + result = (char*) malloc(len + 1); + if (result == 0) { + JNU_ThrowByName(env, "java/lang/OutOfMemoryError", 0); + env->DeleteLocalRef(bytes); + return 0; + } + env->GetByteArrayRegion(bytes, 0, len, (jbyte*) result); + result[len] = 0; /* NULL-terminate */ + } else { + env->DeleteLocalRef(exc); + } + env->DeleteLocalRef(bytes); + return result; +} + +int jniGetFDFromFileDescriptor(JNIEnv* env, jobject fileDescriptor) { + jclass Class_java_io_FileDescriptor = env->FindClass("java/io/FileDescriptor"); + jfieldID descriptor = env->GetFieldID(Class_java_io_FileDescriptor, + "descriptor", "I"); + return env->GetIntField(fileDescriptor, descriptor); +} + +static int create_subprocess( + const char* cmd, const char* arg0, const char* arg1, int* pProcessId) { + char* devname; + int ptm; + pid_t pid; + + ptm = open("/dev/ptmx", O_RDWR); // | O_NOCTTY); + if(ptm < 0){ + LOG("[ cannot open /dev/ptmx - %s ]\n", strerror(errno)); + return -1; + } + fcntl(ptm, F_SETFD, FD_CLOEXEC); + + if(grantpt(ptm) || unlockpt(ptm) || + ((devname = (char*) ptsname(ptm)) == 0)){ + LOG("[ trouble with /dev/ptmx - %s ]\n", strerror(errno)); + return -1; + } + + pid = fork(); + if(pid < 0) { + LOG("- fork failed: %s -\n", strerror(errno)); + return -1; + } + + if(pid == 0){ + int pts; + + setsid(); + + pts = open(devname, O_RDWR); + if(pts < 0) exit(-1); + + dup2(pts, 0); + dup2(pts, 1); + dup2(pts, 2); + + close(ptm); + + execl(cmd, cmd, arg0, arg1, NULL); + exit(-1); + } else { + *pProcessId = (int) pid; + return ptm; + } +} + +JNIEXPORT jobject JNICALL Java_com_google_ase_Exec_createSubprocess( + JNIEnv* env, jclass clazz, jstring cmd, jstring arg0, jstring arg1, + jintArray processIdArray) { + char* cmd_8 = JNU_GetStringNativeChars(env, cmd); + char* arg0_8 = JNU_GetStringNativeChars(env, arg0); + char* arg1_8 = JNU_GetStringNativeChars(env, arg1); + + int procId; + int ptm = create_subprocess(cmd_8, arg0_8, arg1_8, &procId); + + if (processIdArray) { + int procIdLen = env->GetArrayLength(processIdArray); + if (procIdLen > 0) { + jboolean isCopy; + int* pProcId = (int*) env->GetPrimitiveArrayCritical(processIdArray, &isCopy); + if (pProcId) { + *pProcId = procId; + env->ReleasePrimitiveArrayCritical(processIdArray, pProcId, 0); + } + } + } + + jclass Class_java_io_FileDescriptor = env->FindClass("java/io/FileDescriptor"); + jmethodID init = env->GetMethodID(Class_java_io_FileDescriptor, + "", "()V"); + jobject result = env->NewObject(Class_java_io_FileDescriptor, init); + + if (!result) { + LOG("Couldn't create a FileDescriptor."); + } else { + jfieldID descriptor = env->GetFieldID(Class_java_io_FileDescriptor, + "descriptor", "I"); + env->SetIntField(result, descriptor, ptm); + } + + return result; +} + +JNIEXPORT void Java_com_google_ase_Exec_setPtyWindowSize( + JNIEnv* env, jclass clazz, jobject fileDescriptor, jint row, jint col, + jint xpixel, jint ypixel) { + int fd; + struct winsize sz; + + fd = jniGetFDFromFileDescriptor(env, fileDescriptor); + + if (env->ExceptionOccurred() != NULL) { + return; + } + + sz.ws_row = row; + sz.ws_col = col; + sz.ws_xpixel = xpixel; + sz.ws_ypixel = ypixel; + + ioctl(fd, TIOCSWINSZ, &sz); +} + +JNIEXPORT jint Java_com_google_ase_Exec_waitFor(JNIEnv* env, jclass clazz, + jint procId) { + int status; + waitpid(procId, &status, 0); + int result = 0; + if (WIFEXITED(status)) { + result = WEXITSTATUS(status); + } + return result; +} diff --git a/app/src/main/jni/com_google_ase_Exec.h b/app/src/main/jni/com_google_ase_Exec.h new file mode 100644 index 0000000..a2a6052 --- /dev/null +++ b/app/src/main/jni/com_google_ase_Exec.h @@ -0,0 +1,45 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_google_ase_Exec */ + +#ifndef _Included_com_google_ase_Exec +#define _Included_com_google_ase_Exec +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_google_ase_Exec + * Method: createSubprocess + * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[I)Ljava/io/FileDescriptor; + */ +JNIEXPORT jobject JNICALL Java_com_google_ase_Exec_createSubprocess + (JNIEnv *, jclass, jstring, jstring, jstring, jintArray); + +/* + * Class: com_google_ase_Exec + * Method: setPtyWindowSize + * Signature: (Ljava/io/FileDescriptor;IIII)V + */ +JNIEXPORT void JNICALL Java_com_google_ase_Exec_setPtyWindowSize + (JNIEnv *, jclass, jobject, jint, jint, jint, jint); + +/* + * Class: com_google_ase_Exec + * Method: waitFor + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_com_google_ase_Exec_waitFor + (JNIEnv *, jclass, jint); + +/* + * Class: com_google_ase_Exec + * Method: register + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_com_google_ase_Exec_register + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/app/src/main/res/anim/fade_out_delayed.xml b/app/src/main/res/anim/fade_out_delayed.xml new file mode 100644 index 0000000..20ca839 --- /dev/null +++ b/app/src/main/res/anim/fade_out_delayed.xml @@ -0,0 +1,27 @@ + + + + diff --git a/app/src/main/res/anim/fade_stay_hidden.xml b/app/src/main/res/anim/fade_stay_hidden.xml new file mode 100644 index 0000000..e62ca8b --- /dev/null +++ b/app/src/main/res/anim/fade_stay_hidden.xml @@ -0,0 +1,26 @@ + + + + diff --git a/app/src/main/res/anim/keyboard_fade_in.xml b/app/src/main/res/anim/keyboard_fade_in.xml new file mode 100644 index 0000000..edd5b94 --- /dev/null +++ b/app/src/main/res/anim/keyboard_fade_in.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/anim/keyboard_fade_out.xml b/app/src/main/res/anim/keyboard_fade_out.xml new file mode 100644 index 0000000..1f37d32 --- /dev/null +++ b/app/src/main/res/anim/keyboard_fade_out.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/anim/slide_left_in.xml b/app/src/main/res/anim/slide_left_in.xml new file mode 100644 index 0000000..29a0048 --- /dev/null +++ b/app/src/main/res/anim/slide_left_in.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/anim/slide_left_out.xml b/app/src/main/res/anim/slide_left_out.xml new file mode 100644 index 0000000..9c46442 --- /dev/null +++ b/app/src/main/res/anim/slide_left_out.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/anim/slide_right_in.xml b/app/src/main/res/anim/slide_right_in.xml new file mode 100644 index 0000000..0d52c9f --- /dev/null +++ b/app/src/main/res/anim/slide_right_in.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/anim/slide_right_out.xml b/app/src/main/res/anim/slide_right_out.xml new file mode 100644 index 0000000..ace4e9d --- /dev/null +++ b/app/src/main/res/anim/slide_right_out.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/color/blue.xml b/app/src/main/res/color/blue.xml new file mode 100644 index 0000000..981c3ef --- /dev/null +++ b/app/src/main/res/color/blue.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/app/src/main/res/color/green.xml b/app/src/main/res/color/green.xml new file mode 100644 index 0000000..388ea58 --- /dev/null +++ b/app/src/main/res/color/green.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/app/src/main/res/color/red.xml b/app/src/main/res/color/red.xml new file mode 100644 index 0000000..b7e18cd --- /dev/null +++ b/app/src/main/res/color/red.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable-hdpi/icon.png b/app/src/main/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..f5154b3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon.png differ diff --git a/app/src/main/res/drawable-hdpi/notification_icon.png b/app/src/main/res/drawable-hdpi/notification_icon.png new file mode 100644 index 0000000..518f990 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_icon.png differ diff --git a/app/src/main/res/drawable-mdpi-v6/icon.png b/app/src/main/res/drawable-mdpi-v6/icon.png new file mode 100644 index 0000000..e701f14 Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v6/icon.png differ diff --git a/app/src/main/res/drawable-mdpi/icon.png b/app/src/main/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..8a7202d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/icon.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_icon.png b/app/src/main/res/drawable-mdpi/notification_icon.png new file mode 100644 index 0000000..01f6dca Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_icon.png differ diff --git a/app/src/main/res/drawable/button_ctrl.png b/app/src/main/res/drawable/button_ctrl.png new file mode 100644 index 0000000..dc4d786 Binary files /dev/null and b/app/src/main/res/drawable/button_ctrl.png differ diff --git a/app/src/main/res/drawable/button_esc.png b/app/src/main/res/drawable/button_esc.png new file mode 100644 index 0000000..5f0cfc6 Binary files /dev/null and b/app/src/main/res/drawable/button_esc.png differ diff --git a/app/src/main/res/drawable/button_keyboard.png b/app/src/main/res/drawable/button_keyboard.png new file mode 100644 index 0000000..9205d8b Binary files /dev/null and b/app/src/main/res/drawable/button_keyboard.png differ diff --git a/app/src/main/res/drawable/connected.xml b/app/src/main/res/drawable/connected.xml new file mode 100644 index 0000000..7b633d3 --- /dev/null +++ b/app/src/main/res/drawable/connected.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/highlight_disabled_pressed.9.png b/app/src/main/res/drawable/highlight_disabled_pressed.9.png new file mode 100644 index 0000000..807fcb5 Binary files /dev/null and b/app/src/main/res/drawable/highlight_disabled_pressed.9.png differ diff --git a/app/src/main/res/drawable/ic_btn_back.png b/app/src/main/res/drawable/ic_btn_back.png new file mode 100644 index 0000000..9615e67 Binary files /dev/null and b/app/src/main/res/drawable/ic_btn_back.png differ diff --git a/app/src/main/res/drawable/ic_btn_next.png b/app/src/main/res/drawable/ic_btn_next.png new file mode 100644 index 0000000..ea2a80e Binary files /dev/null and b/app/src/main/res/drawable/ic_btn_next.png differ diff --git a/app/src/main/res/drawable/icon_older.png b/app/src/main/res/drawable/icon_older.png new file mode 100644 index 0000000..323b99f Binary files /dev/null and b/app/src/main/res/drawable/icon_older.png differ diff --git a/app/src/main/res/drawable/pubkey.xml b/app/src/main/res/drawable/pubkey.xml new file mode 100644 index 0000000..1926d24 --- /dev/null +++ b/app/src/main/res/drawable/pubkey.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/pubkey_locked.png b/app/src/main/res/drawable/pubkey_locked.png new file mode 100644 index 0000000..7df56b5 Binary files /dev/null and b/app/src/main/res/drawable/pubkey_locked.png differ diff --git a/app/src/main/res/drawable/pubkey_unlocked.png b/app/src/main/res/drawable/pubkey_unlocked.png new file mode 100644 index 0000000..eec4558 Binary files /dev/null and b/app/src/main/res/drawable/pubkey_unlocked.png differ diff --git a/app/src/main/res/layout-land/item_host.xml b/app/src/main/res/layout-land/item_host.xml new file mode 100644 index 0000000..9cde624 --- /dev/null +++ b/app/src/main/res/layout-land/item_host.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-port/item_host.xml b/app/src/main/res/layout-port/item_host.xml new file mode 100644 index 0000000..24d34ba --- /dev/null +++ b/app/src/main/res/layout-port/item_host.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/act_colors.xml b/app/src/main/res/layout/act_colors.xml new file mode 100644 index 0000000..625437d --- /dev/null +++ b/app/src/main/res/layout/act_colors.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/act_console.xml b/app/src/main/res/layout/act_console.xml new file mode 100644 index 0000000..f7db787 --- /dev/null +++ b/app/src/main/res/layout/act_console.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + +