aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2014-10-01 23:04:51 +0100
committerKenny Root <kenny@the-b.org>2014-10-01 12:48:19 +0100
commit49b779dcaf03e3598d2709b321e20ea029b25163 (patch)
tree05af547b1f1433d7dd6f7373d0b25a455e053a03 /app/src/main/java
parentd64786d9197090c74072b648e487e3d34817bb57 (diff)
downloadconnectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.gz
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.tar.bz2
connectbot-49b779dcaf03e3598d2709b321e20ea029b25163.zip
Convert to gradle build system
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/google/ase/Exec.java69
-rw-r--r--app/src/main/java/com/jcraft/jzlib/Adler32.java94
-rw-r--r--app/src/main/java/com/jcraft/jzlib/Deflate.java1623
-rw-r--r--app/src/main/java/com/jcraft/jzlib/InfBlocks.java614
-rw-r--r--app/src/main/java/com/jcraft/jzlib/InfCodes.java605
-rw-r--r--app/src/main/java/com/jcraft/jzlib/InfTree.java520
-rw-r--r--app/src/main/java/com/jcraft/jzlib/Inflate.java374
-rw-r--r--app/src/main/java/com/jcraft/jzlib/JZlib.java67
-rw-r--r--app/src/main/java/com/jcraft/jzlib/StaticTree.java149
-rw-r--r--app/src/main/java/com/jcraft/jzlib/Tree.java365
-rw-r--r--app/src/main/java/com/jcraft/jzlib/ZInputStream.java149
-rw-r--r--app/src/main/java/com/jcraft/jzlib/ZOutputStream.java156
-rw-r--r--app/src/main/java/com/jcraft/jzlib/ZStream.java211
-rw-r--r--app/src/main/java/com/jcraft/jzlib/ZStreamException.java44
-rw-r--r--app/src/main/java/com/trilead/ssh2/AuthAgentCallback.java64
-rw-r--r--app/src/main/java/com/trilead/ssh2/ChannelCondition.java61
-rw-r--r--app/src/main/java/com/trilead/ssh2/Connection.java1628
-rw-r--r--app/src/main/java/com/trilead/ssh2/ConnectionInfo.java53
-rw-r--r--app/src/main/java/com/trilead/ssh2/ConnectionMonitor.java34
-rw-r--r--app/src/main/java/com/trilead/ssh2/DHGexParameters.java121
-rw-r--r--app/src/main/java/com/trilead/ssh2/DebugLogger.java23
-rw-r--r--app/src/main/java/com/trilead/ssh2/DynamicPortForwarder.java67
-rw-r--r--app/src/main/java/com/trilead/ssh2/HTTPProxyData.java83
-rw-r--r--app/src/main/java/com/trilead/ssh2/HTTPProxyException.java29
-rw-r--r--app/src/main/java/com/trilead/ssh2/InteractiveCallback.java55
-rw-r--r--app/src/main/java/com/trilead/ssh2/KnownHosts.java844
-rw-r--r--app/src/main/java/com/trilead/ssh2/LocalPortForwarder.java63
-rw-r--r--app/src/main/java/com/trilead/ssh2/LocalStreamForwarder.java78
-rw-r--r--app/src/main/java/com/trilead/ssh2/ProxyData.java15
-rw-r--r--app/src/main/java/com/trilead/ssh2/SCPClient.java729
-rw-r--r--app/src/main/java/com/trilead/ssh2/SFTPException.java91
-rw-r--r--app/src/main/java/com/trilead/ssh2/SFTPv3Client.java1388
-rw-r--r--app/src/main/java/com/trilead/ssh2/SFTPv3DirectoryEntry.java38
-rw-r--r--app/src/main/java/com/trilead/ssh2/SFTPv3FileAttributes.java145
-rw-r--r--app/src/main/java/com/trilead/ssh2/SFTPv3FileHandle.java45
-rw-r--r--app/src/main/java/com/trilead/ssh2/ServerHostKeyVerifier.java31
-rw-r--r--app/src/main/java/com/trilead/ssh2/Session.java530
-rw-r--r--app/src/main/java/com/trilead/ssh2/StreamGobbler.java229
-rw-r--r--app/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java466
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/AuthAgentForwardThread.java579
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/Channel.java207
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/ChannelInputStream.java86
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/ChannelManager.java1756
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/ChannelOutputStream.java71
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/DynamicAcceptThread.java284
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/IChannelWorkerThread.java13
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/LocalAcceptThread.java135
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/RemoteAcceptThread.java103
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/RemoteForwardingData.java17
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/RemoteX11AcceptThread.java240
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/StreamForwarder.java113
-rw-r--r--app/src/main/java/com/trilead/ssh2/channel/X11ServerData.java16
-rw-r--r--app/src/main/java/com/trilead/ssh2/compression/CompressionFactory.java96
-rw-r--r--app/src/main/java/com/trilead/ssh2/compression/ICompressor.java32
-rw-r--r--app/src/main/java/com/trilead/ssh2/compression/Zlib.java130
-rw-r--r--app/src/main/java/com/trilead/ssh2/compression/ZlibOpenSSH.java35
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/Base64.java148
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/CryptoWishList.java26
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/KeyMaterial.java91
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/PEMDecoder.java494
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/PEMStructure.java17
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/SimpleDERReader.java229
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/AES.java698
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipher.java16
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/BlockCipherFactory.java115
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/BlowFish.java403
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/CBCMode.java78
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/CTRMode.java62
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherInputStream.java144
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/CipherOutputStream.java142
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/DES.java373
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/DESede.java105
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/cipher/NullCipher.java35
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/dh/DhExchange.java132
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/dh/DhGroupExchange.java113
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/dh/EcDhExchange.java106
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/dh/GenericDhExchange.java93
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java91
-rw-r--r--app/src/main/java/com/trilead/ssh2/crypto/digest/MAC.java157
-rw-r--r--app/src/main/java/com/trilead/ssh2/log/Logger.java54
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java33
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenConfirmation.java66
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketChannelOpenFailure.java66
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketChannelTrileadPing.java35
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketChannelWindowAdjust.java57
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketDisconnect.java57
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketGlobalCancelForwardRequest.java42
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketGlobalForwardRequest.java41
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketGlobalTrileadPing.java32
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketIgnore.java59
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDHInit.java31
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDHReply.java53
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexGroup.java50
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexInit.java33
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexReply.java56
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequest.java39
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexDhGexRequestOld.java34
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketKexInit.java165
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketNewKeys.java46
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketOpenDirectTCPIPChannel.java56
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketOpenSessionChannel.java62
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketServiceAccept.java61
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketServiceRequest.java52
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketSessionExecCommand.java39
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyRequest.java57
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketSessionPtyResize.java40
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketSessionStartShell.java36
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketSessionSubsystemRequest.java40
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketSessionX11Request.java53
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthBanner.java60
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthFailure.java53
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoRequest.java84
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthInfoResponse.java35
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestInteractive.java42
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestNone.java61
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPassword.java67
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/PacketUserauthRequestPublicKey.java65
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/Packets.java149
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/TypesReader.java177
-rw-r--r--app/src/main/java/com/trilead/ssh2/packets/TypesWriter.java169
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/AttrTextHints.java38
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/AttribBits.java129
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/AttribFlags.java112
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/AttribPermissions.java32
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/AttribTypes.java28
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/ErrorCodes.java104
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/OpenFlags.java223
-rw-r--r--app/src/main/java/com/trilead/ssh2/sftp/Packet.java43
-rw-r--r--app/src/main/java/com/trilead/ssh2/signature/DSASHA1Verify.java255
-rw-r--r--app/src/main/java/com/trilead/ssh2/signature/ECDSASHA2Verify.java487
-rw-r--r--app/src/main/java/com/trilead/ssh2/signature/RSASHA1Verify.java180
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java125
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/KexManager.java671
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/KexParameters.java24
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/KexState.java33
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/MessageHandler.java14
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/NegotiateException.java12
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java22
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/TransportConnection.java343
-rw-r--r--app/src/main/java/com/trilead/ssh2/transport/TransportManager.java802
-rw-r--r--app/src/main/java/com/trilead/ssh2/util/TimeoutService.java149
-rw-r--r--app/src/main/java/com/trilead/ssh2/util/Tokenizer.java51
-rw-r--r--app/src/main/java/de/mud/telnet/TelnetProtocolHandler.java678
-rw-r--r--app/src/main/java/de/mud/terminal/Precomposer.java1052
-rw-r--r--app/src/main/java/de/mud/terminal/VDUBuffer.java854
-rw-r--r--app/src/main/java/de/mud/terminal/VDUDisplay.java40
-rw-r--r--app/src/main/java/de/mud/terminal/VDUInput.java90
-rw-r--r--app/src/main/java/de/mud/terminal/vt320.java3032
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Authentication.java34
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/AuthenticationNone.java17
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Proxy.java404
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/ProxyMessage.java109
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/ProxyServer.java591
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Socks4Message.java171
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Socks4Proxy.java107
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Socks5DatagramSocket.java460
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Socks5Message.java292
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/Socks5Proxy.java231
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/SocksException.java80
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/SocksServerSocket.java164
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/SocksSocket.java291
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/UDPEncapsulation.java29
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/UDPRelayServer.java212
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticator.java120
-rw-r--r--app/src/main/java/net/sourceforge/jsocks/server/ServerAuthenticatorNone.java169
-rw-r--r--app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java423
-rw-r--r--app/src/main/java/org/connectbot/ActionBarWrapper.java83
-rw-r--r--app/src/main/java/org/connectbot/ColorsActivity.java331
-rw-r--r--app/src/main/java/org/connectbot/ConsoleActivity.java1175
-rw-r--r--app/src/main/java/org/connectbot/GeneratePubkeyActivity.java353
-rw-r--r--app/src/main/java/org/connectbot/HelpActivity.java77
-rw-r--r--app/src/main/java/org/connectbot/HelpTopicActivity.java49
-rw-r--r--app/src/main/java/org/connectbot/HostEditorActivity.java433
-rw-r--r--app/src/main/java/org/connectbot/HostListActivity.java570
-rw-r--r--app/src/main/java/org/connectbot/PortForwardListActivity.java425
-rw-r--r--app/src/main/java/org/connectbot/PubkeyListActivity.java673
-rw-r--r--app/src/main/java/org/connectbot/SettingsActivity.java60
-rw-r--r--app/src/main/java/org/connectbot/StrictModeSetup.java23
-rw-r--r--app/src/main/java/org/connectbot/TerminalView.java452
-rw-r--r--app/src/main/java/org/connectbot/WizardActivity.java105
-rw-r--r--app/src/main/java/org/connectbot/bean/AbstractBean.java49
-rw-r--r--app/src/main/java/org/connectbot/bean/HostBean.java317
-rw-r--r--app/src/main/java/org/connectbot/bean/PortForwardBean.java239
-rw-r--r--app/src/main/java/org/connectbot/bean/PubkeyBean.java234
-rw-r--r--app/src/main/java/org/connectbot/bean/SelectionArea.java201
-rw-r--r--app/src/main/java/org/connectbot/service/BackupAgent.java73
-rw-r--r--app/src/main/java/org/connectbot/service/BackupWrapper.java71
-rw-r--r--app/src/main/java/org/connectbot/service/BridgeDisconnectedListener.java22
-rw-r--r--app/src/main/java/org/connectbot/service/ConnectionNotifier.java192
-rw-r--r--app/src/main/java/org/connectbot/service/ConnectivityReceiver.java154
-rw-r--r--app/src/main/java/org/connectbot/service/FontSizeChangedListener.java31
-rw-r--r--app/src/main/java/org/connectbot/service/KeyEventUtil.java98
-rw-r--r--app/src/main/java/org/connectbot/service/PromptHelper.java159
-rw-r--r--app/src/main/java/org/connectbot/service/Relay.java145
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalBridge.java1018
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalKeyListener.java558
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalManager.java714
-rw-r--r--app/src/main/java/org/connectbot/transport/AbsTransport.java254
-rw-r--r--app/src/main/java/org/connectbot/transport/Local.java219
-rw-r--r--app/src/main/java/org/connectbot/transport/SSH.java958
-rw-r--r--app/src/main/java/org/connectbot/transport/Telnet.java330
-rw-r--r--app/src/main/java/org/connectbot/transport/TransportFactory.java132
-rw-r--r--app/src/main/java/org/connectbot/util/Colors.java91
-rw-r--r--app/src/main/java/org/connectbot/util/EastAsianWidth.java75
-rw-r--r--app/src/main/java/org/connectbot/util/Encryptor.java205
-rw-r--r--app/src/main/java/org/connectbot/util/EntropyDialog.java50
-rw-r--r--app/src/main/java/org/connectbot/util/EntropyView.java169
-rw-r--r--app/src/main/java/org/connectbot/util/HelpTopicView.java62
-rw-r--r--app/src/main/java/org/connectbot/util/HostDatabase.java766
-rw-r--r--app/src/main/java/org/connectbot/util/OnDbWrittenListener.java26
-rw-r--r--app/src/main/java/org/connectbot/util/OnEntropyGatheredListener.java22
-rw-r--r--app/src/main/java/org/connectbot/util/PreferenceConstants.java90
-rw-r--r--app/src/main/java/org/connectbot/util/PubkeyDatabase.java329
-rw-r--r--app/src/main/java/org/connectbot/util/PubkeyUtils.java352
-rw-r--r--app/src/main/java/org/connectbot/util/RobustSQLiteOpenHelper.java133
-rw-r--r--app/src/main/java/org/connectbot/util/UberColorPickerDialog.java982
-rw-r--r--app/src/main/java/org/connectbot/util/VolumePreference.java72
-rw-r--r--app/src/main/java/org/connectbot/util/XmlBuilder.java71
-rw-r--r--app/src/main/java/org/keyczar/jce/EcCore.java679
-rw-r--r--app/src/main/java/org/openintents/intents/FileManagerIntents.java66
220 files changed, 50982 insertions, 0 deletions
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<NMAX?len:NMAX;
+ len-=k;
+ while(k>=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<hash_size-1; i++){
+ head[i]=0;
+ }
+
+ // Set the default configuration parameters:
+ max_lazy_match = Deflate.config_table[level].max_lazy;
+ good_match = Deflate.config_table[level].good_length;
+ nice_match = Deflate.config_table[level].nice_length;
+ max_chain_length = Deflate.config_table[level].max_chain;
+
+ strstart = 0;
+ block_start = 0;
+ lookahead = 0;
+ match_length = prev_length = MIN_MATCH-1;
+ match_available = 0;
+ ins_h = 0;
+ }
+
+ // Initialize the tree data structures for a new zlib stream.
+ void tr_init(){
+
+ l_desc.dyn_tree = dyn_ltree;
+ l_desc.stat_desc = StaticTree.static_l_desc;
+
+ d_desc.dyn_tree = dyn_dtree;
+ d_desc.stat_desc = StaticTree.static_d_desc;
+
+ bl_desc.dyn_tree = bl_tree;
+ bl_desc.stat_desc = StaticTree.static_bl_desc;
+
+ bi_buf = 0;
+ bi_valid = 0;
+ last_eob_len = 8; // enough lookahead for inflate
+
+ // Initialize the first block of the first file:
+ init_block();
+ }
+
+ void init_block(){
+ // Initialize the trees.
+ for(int i = 0; i < L_CODES; i++) dyn_ltree[i*2] = 0;
+ for(int i= 0; i < D_CODES; i++) dyn_dtree[i*2] = 0;
+ for(int i= 0; i < BL_CODES; i++) bl_tree[i*2] = 0;
+
+ dyn_ltree[END_BLOCK*2] = 1;
+ opt_len = static_len = 0;
+ last_lit = matches = 0;
+ }
+
+ // Restore the heap property by moving down the tree starting at node k,
+ // exchanging a node with the smallest of its two sons if necessary, stopping
+ // when the heap property is re-established (each father smaller than its
+ // two sons).
+ void pqdownheap(short[] tree, // the tree to restore
+ int k // node to move down
+ ){
+ int v = heap[k];
+ int j = k << 1; // left son of k
+ while (j <= heap_len) {
+ // Set j to the smallest of the two sons:
+ if (j < heap_len &&
+ smaller(tree, heap[j+1], heap[j], depth)){
+ j++;
+ }
+ // Exit if v is smaller than both sons
+ if(smaller(tree, v, heap[j], depth)) break;
+
+ // Exchange v with the smallest son
+ heap[k]=heap[j]; k = j;
+ // And continue down the tree, setting j to the left son of k
+ j <<= 1;
+ }
+ heap[k] = v;
+ }
+
+ static boolean smaller(short[] tree, int n, int m, byte[] depth){
+ short tn2=tree[n*2];
+ short tm2=tree[m*2];
+ return (tn2<tm2 ||
+ (tn2==tm2 && depth[n] <= depth[m]));
+ }
+
+ // Scan a literal or distance tree to determine the frequencies of the codes
+ // in the bit length tree.
+ void scan_tree (short[] tree,// the tree to be scanned
+ 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; }
+ tree[(max_code+1)*2+1] = (short)0xffff; // guard
+
+ 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) {
+ bl_tree[curlen*2] += count;
+ }
+ else if(curlen != 0) {
+ if(curlen != prevlen) bl_tree[curlen*2]++;
+ bl_tree[REP_3_6*2]++;
+ }
+ else if(count <= 10) {
+ bl_tree[REPZ_3_10*2]++;
+ }
+ else{
+ bl_tree[REPZ_11_138*2]++;
+ }
+ 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;
+ }
+ }
+ }
+
+ // Construct the Huffman tree for the bit lengths and return the index in
+ // bl_order of the last bit length code to send.
+ int build_bl_tree(){
+ int max_blindex; // index of last bit length code of non zero freq
+
+ // Determine the bit length frequencies for literal and distance trees
+ scan_tree(dyn_ltree, l_desc.max_code);
+ scan_tree(dyn_dtree, d_desc.max_code);
+
+ // Build the bit length tree:
+ bl_desc.build_tree(this);
+ // opt_len now includes the length of the tree representations, except
+ // the lengths of the bit lengths codes and the 5+5+4 bits for the counts.
+
+ // Determine the number of bit length codes to send. The pkzip format
+ // requires that at least 4 bit length codes be sent. (appnote.txt says
+ // 3 but the actual value used is 4.)
+ for (max_blindex = BL_CODES-1; max_blindex >= 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<LITERALS){ bin_freq += dyn_ltree[n*2]; n++;}
+ data_type=(byte)(bin_freq > (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)<<hash_shift)^(window[strstart+1]&0xff))&hash_mask;
+ }
+ // If the whole input has less than MIN_MATCH bytes, ins_h is garbage,
+ // but this is not important since only literal bytes will be emitted.
+ }
+ while (lookahead < MIN_LOOKAHEAD && strm.avail_in != 0);
+ }
+
+ // Compress as much as possible from the input stream, return the current
+ // block state.
+ // This function does not perform lazy evaluation of matches and inserts
+ // new strings in the dictionary only for unmatched strings or for short
+ // matches. It is used only for the fast compression options.
+ int deflate_fast(int flush){
+// short hash_head = 0; // head of the hash chain
+ int hash_head = 0; // head of the hash chain
+ boolean bflush; // set if current block must be flushed
+
+ while(true){
+ // Make sure that we always have enough lookahead, except
+ // at the end of the input file. We need MAX_MATCH bytes
+ // for the next match, plus MIN_MATCH bytes to insert the
+ // string following the next match.
+ if(lookahead < MIN_LOOKAHEAD){
+ fill_window();
+ if(lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH){
+ return NeedMore;
+ }
+ if(lookahead == 0) break; // flush the current block
+ }
+
+ // Insert the string window[strstart .. strstart+2] in the
+ // dictionary, and set hash_head to the head of the hash chain:
+ if(lookahead >= MIN_MATCH){
+ ins_h=(((ins_h)<<hash_shift)^(window[(strstart)+(MIN_MATCH-1)]&0xff))&hash_mask;
+
+// prev[strstart&w_mask]=hash_head=head[ins_h];
+ hash_head=(head[ins_h]&0xffff);
+ prev[strstart&w_mask]=head[ins_h];
+ head[ins_h]=(short)strstart;
+ }
+
+ // Find the longest match, discarding those <= prev_length.
+ // At this point we have always match_length < MIN_MATCH
+
+ if(hash_head!=0L &&
+ ((strstart-hash_head)&0xffff) <= w_size-MIN_LOOKAHEAD
+ ){
+ // To simplify the code, we prevent matches with the string
+ // of window index 0 (in particular we have to avoid a match
+ // of the string with itself at the start of the input file).
+ if(strategy != Z_HUFFMAN_ONLY){
+ match_length=longest_match (hash_head);
+ }
+ // longest_match() sets match_start
+ }
+ if(match_length>=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<<hash_shift)^(window[(strstart)+(MIN_MATCH-1)]&0xff))&hash_mask;
+// prev[strstart&w_mask]=hash_head=head[ins_h];
+ hash_head=(head[ins_h]&0xffff);
+ prev[strstart&w_mask]=head[ins_h];
+ head[ins_h]=(short)strstart;
+
+ // strstart never exceeds WSIZE-MAX_MATCH, so there are
+ // always MIN_MATCH bytes ahead.
+ }
+ while (--match_length != 0);
+ strstart++;
+ }
+ else{
+ strstart += match_length;
+ match_length = 0;
+ ins_h = window[strstart]&0xff;
+
+ ins_h=(((ins_h)<<hash_shift)^(window[strstart+1]&0xff))&hash_mask;
+ // If lookahead < MIN_MATCH, ins_h is garbage, but it does not
+ // matter since it will be recomputed at next deflate call.
+ }
+ }
+ else {
+ // No match, output a literal byte
+
+ bflush=_tr_tally(0, window[strstart]&0xff);
+ lookahead--;
+ strstart++;
+ }
+ if (bflush){
+
+ flush_block_only(false);
+ if(strm.avail_out==0) return NeedMore;
+ }
+ }
+
+ flush_block_only(flush == Z_FINISH);
+ if(strm.avail_out==0){
+ if(flush == Z_FINISH) return FinishStarted;
+ else return NeedMore;
+ }
+ return flush==Z_FINISH ? FinishDone : BlockDone;
+ }
+
+ // Same as above, but achieves better compression. We use a lazy
+ // evaluation for matches: a match is finally adopted only if there is
+ // no better match at the next window position.
+ int deflate_slow(int flush){
+// short hash_head = 0; // head of hash chain
+ int hash_head = 0; // head of hash chain
+ boolean bflush; // set if current block must be flushed
+
+ // Process the input block.
+ while(true){
+ // Make sure that we always have enough lookahead, except
+ // at the end of the input file. We need MAX_MATCH bytes
+ // for the next match, plus MIN_MATCH bytes to insert the
+ // string following the next match.
+
+ if (lookahead < MIN_LOOKAHEAD) {
+ fill_window();
+ if(lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) {
+ return NeedMore;
+ }
+ if(lookahead == 0) break; // flush the current block
+ }
+
+ // Insert the string window[strstart .. strstart+2] in the
+ // dictionary, and set hash_head to the head of the hash chain:
+
+ if(lookahead >= MIN_MATCH) {
+ ins_h=(((ins_h)<<hash_shift)^(window[(strstart)+(MIN_MATCH-1)]&0xff)) & hash_mask;
+// prev[strstart&w_mask]=hash_head=head[ins_h];
+ hash_head=(head[ins_h]&0xffff);
+ prev[strstart&w_mask]=head[ins_h];
+ head[ins_h]=(short)strstart;
+ }
+
+ // Find the longest match, discarding those <= prev_length.
+ prev_length = match_length; prev_match = match_start;
+ match_length = MIN_MATCH-1;
+
+ if (hash_head != 0 && prev_length < max_lazy_match &&
+ ((strstart-hash_head)&0xffff) <= w_size-MIN_LOOKAHEAD
+ ){
+ // To simplify the code, we prevent matches with the string
+ // of window index 0 (in particular we have to avoid a match
+ // of the string with itself at the start of the input file).
+
+ if(strategy != Z_HUFFMAN_ONLY) {
+ match_length = longest_match(hash_head);
+ }
+ // longest_match() sets match_start
+
+ if (match_length <= 5 && (strategy == Z_FILTERED ||
+ (match_length == MIN_MATCH &&
+ strstart - match_start > 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)<<hash_shift)^(window[(strstart)+(MIN_MATCH-1)]&0xff))&hash_mask;
+ //prev[strstart&w_mask]=hash_head=head[ins_h];
+ hash_head=(head[ins_h]&0xffff);
+ prev[strstart&w_mask]=head[ins_h];
+ head[ins_h]=(short)strstart;
+ }
+ }
+ while(--prev_length != 0);
+ match_available = 0;
+ match_length = MIN_MATCH-1;
+ strstart++;
+
+ if (bflush){
+ flush_block_only(false);
+ if(strm.avail_out==0) return NeedMore;
+ }
+ } else if (match_available!=0) {
+
+ // If there was no match at the previous position, output a
+ // single literal. If there was a match but the current match
+ // is longer, truncate the previous match to a single literal.
+
+ bflush=_tr_tally(0, window[strstart-1]&0xff);
+
+ if (bflush) {
+ flush_block_only(false);
+ }
+ strstart++;
+ lookahead--;
+ if(strm.avail_out == 0) return NeedMore;
+ } else {
+ // There is no previous match to compare with, wait for
+ // the next step to decide.
+
+ match_available = 1;
+ strstart++;
+ lookahead--;
+ }
+ }
+
+ if(match_available!=0) {
+ bflush=_tr_tally(0, window[strstart-1]&0xff);
+ match_available = 0;
+ }
+ flush_block_only(flush == Z_FINISH);
+
+ if(strm.avail_out==0){
+ if(flush == Z_FINISH) return FinishStarted;
+ else return NeedMore;
+ }
+
+ return flush == Z_FINISH ? FinishDone : BlockDone;
+ }
+
+ int longest_match(int cur_match){
+ int chain_length = max_chain_length; // max hash chain length
+ int scan = strstart; // current string
+ int match; // matched string
+ int len; // length of current match
+ int best_len = prev_length; // best match length so far
+ int limit = strstart>(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)<<hash_shift)^(window[1]&0xff))&hash_mask;
+
+ for(int n=0; n<=length-MIN_MATCH; n++){
+ ins_h=(((ins_h)<<hash_shift)^(window[(n)+(MIN_MATCH-1)]&0xff))&hash_mask;
+ prev[n&w_mask]=head[ins_h];
+ head[ins_h]=(short)n;
+ }
+ return Z_OK;
+ }
+
+ int deflate(ZStream strm, int flush){
+ int old_flush;
+
+ if(flush>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<hash_size/*-1*/; i++) // forget history
+ head[i]=0;
+ }
+ }
+ strm.flush_pending();
+ if(strm.avail_out == 0) {
+ last_flush = -1; // avoid BUF_ERROR at next call, see above
+ return Z_OK;
+ }
+ }
+ }
+
+ if(flush!=Z_FINISH) return Z_OK;
+ if(noheader!=0) return Z_STREAM_END;
+
+ // Write the zlib trailer (adler32)
+ putShortMSB((int)(strm.adler>>>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<read?read-q-1:end-q);}
+
+ // process input based on current state
+ while(true){
+ switch (mode){
+ case TYPE:
+
+ 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)<<k;
+ k+=8;
+ }
+ t = (int)(b & 7);
+ last = t & 1;
+
+ switch (t >>> 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)<<k;
+ k+=8;
+ }
+
+ if ((((~b) >>> 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)(q<read?read-q-1:end-q);
+ }
+ if(m==0){
+ write=q;
+ r=inflate_flush(z,r);
+ q=write;m=(int)(q<read?read-q-1:end-q);
+ if(q==end&&read!=0){
+ q=0; m=(int)(q<read?read-q-1:end-q);
+ }
+ if(m==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);
+ }
+ }
+ }
+ r=Z_OK;
+
+ t = left;
+ if(t>n) 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)<<k;
+ k+=8;
+ }
+
+ table = t = (b & 0x3fff);
+ if ((t & 0x1f) > 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<t){
+ blens=new int[t];
+ }
+ else{
+ for(int i=0; i<t; i++){blens[i]=0;}
+ }
+
+ {b>>>=(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)<<k;
+ k+=8;
+ }
+
+ blens[border[index++]] = b&7;
+
+ {b>>>=(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)<<k;
+ k+=8;
+ }
+
+ if(tb[0]==-1){
+ //System.err.println("null...");
+ }
+
+ t=hufts[(tb[0]+(b&inflate_mask[t]))*3+1];
+ c=hufts[(tb[0]+(b&inflate_mask[t]))*3+2];
+
+ if (c < 16){
+ b>>>=(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)<<k;
+ k+=8;
+ }
+
+ b>>>=(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<read?read-q-1:end-q);
+
+ if (last==0){
+ mode = TYPE;
+ break;
+ }
+ mode = DRY;
+ case DRY:
+ write=q;
+ r=inflate_flush(z, r);
+ q=write; m=(int)(q<read?read-q-1:end-q);
+ if (read != write){
+ 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);
+ }
+ mode = DONE;
+ case DONE:
+ r = Z_STREAM_END;
+
+ 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);
+ case BAD:
+ 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);
+
+ default:
+ r = Z_STREAM_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);
+ }
+ }
+ }
+
+ void free(ZStream z){
+ reset(z, null);
+ window=null;
+ hufts=null;
+ //ZFREE(z, s);
+ }
+
+ void set_dictionary(byte[] d, int start, int n){
+ System.arraycopy(d, start, window, 0, n);
+ read = write = n;
+ }
+
+ // Returns true if inflate is currently at the end of a block generated
+ // by Z_SYNC_FLUSH or Z_FULL_FLUSH.
+ int sync_point(){
+ return mode == LENS ? 1 : 0;
+ }
+
+ // copy as much as possible from the sliding window to the output area
+ int inflate_flush(ZStream z, int r){
+ int n;
+ int p;
+ int q;
+
+ // local copies of source and destination pointers
+ p = z.next_out_index;
+ q = read;
+
+ // compute number of bytes to copy as far as end of window
+ n = (int)((q <= write ? write : end) - 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 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<s.read?s.read-q-1:s.end-q;
+
+ // process input and output based on current state
+ while (true){
+ switch (mode){
+ // waiting for "i:"=input, "o:"=output, "x:"=nothing
+ case START: // x: set up for LEN
+ if (m >= 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<s.read?s.read-q-1:s.end-q;
+
+ if (r != Z_OK){
+ mode = r == Z_STREAM_END ? WASH : BADCODE;
+ break;
+ }
+ }
+ need = lbits;
+ tree = ltree;
+ tree_index=ltree_index;
+
+ mode = LEN;
+ case LEN: // i: get length/literal/eob 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)<<k;
+ k+=8;
+ }
+
+ tindex=(tree_index+(b&inflate_mask[j]))*3;
+
+ b>>>=(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)<<k;
+ k+=8;
+ }
+
+ len += (b & inflate_mask[j]);
+
+ b>>=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)<<k;
+ k+=8;
+ }
+
+ tindex=(tree_index+(b & inflate_mask[j]))*3;
+
+ b>>=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)<<k;
+ k+=8;
+ }
+
+ dist += (b & inflate_mask[j]);
+
+ b>>=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<s.read?s.read-q-1:s.end-q;}
+ if(m==0){
+ s.write=q; r=s.inflate_flush(z,r);
+ q=s.write;m=q<s.read?s.read-q-1:s.end-q;
+
+ if(q==s.end&&s.read!=0){q=0;m=q<s.read?s.read-q-1:s.end-q;}
+
+ if(m==0){
+ 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);
+ }
+ }
+ }
+
+ s.window[q++]=s.window[f++]; m--;
+
+ if (f == s.end)
+ f = 0;
+ len--;
+ }
+ mode = START;
+ break;
+ case LIT: // o: got literal, waiting for output space
+ if(m==0){
+ if(q==s.end&&s.read!=0){q=0;m=q<s.read?s.read-q-1:s.end-q;}
+ if(m==0){
+ s.write=q; r=s.inflate_flush(z,r);
+ q=s.write;m=q<s.read?s.read-q-1:s.end-q;
+
+ if(q==s.end&&s.read!=0){q=0;m=q<s.read?s.read-q-1:s.end-q;}
+ if(m==0){
+ 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);
+ }
+ }
+ }
+ r=Z_OK;
+
+ s.window[q++]=(byte)lit; m--;
+
+ mode = START;
+ break;
+ case WASH: // o: got eob, possibly more output
+ if (k > 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<s.read?s.read-q-1:s.end-q;
+
+ if (s.read != s.write){
+ 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);
+ }
+ mode = END;
+ case END:
+ r = Z_STREAM_END;
+ 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 BADCODE: // x: got error
+
+ 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);
+
+ default:
+ r = Z_STREAM_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);
+ }
+ }
+ }
+
+ void free(ZStream z){
+ // ZFREE(z, c);
+ }
+
+ // Called with number of bytes left to write in window at least 258
+ // (the maximum string length) and number of input bytes available
+ // at least ten. The ten bytes are six bytes for the longest length/
+ // distance pair plus four bytes for overloading the bit buffer.
+
+ int inflate_fast(int bl, int bd,
+ int[] tl, int tl_index,
+ int[] td, int td_index,
+ InfBlocks s, ZStream z){
+ int t; // temporary pointer
+ int[] tp; // temporary pointer
+ int tp_index; // temporary pointer
+ int e; // extra bits or operation
+ 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
+ int ml; // mask for literal/length tree
+ int md; // mask for distance tree
+ int c; // bytes to copy
+ int d; // distance back to copy from
+ int r; // copy source pointer
+
+ int tp_index_t_3; // (tp_index+t)*3
+
+ // load input, output, bit values
+ p=z.next_in_index;n=z.avail_in;b=s.bitb;k=s.bitk;
+ q=s.write;m=q<s.read?s.read-q-1:s.end-q;
+
+ // initialize masks
+ ml = inflate_mask[bl];
+ md = inflate_mask[bd];
+
+ // do until not enough input or output space for fast loop
+ do { // assume called with m >= 258 && n >= 10
+ // get literal/length code
+ while(k<(20)){ // max bits for literal/length code
+ n--;
+ b|=(z.next_in[p++]&0xff)<<k;k+=8;
+ }
+
+ t= b&ml;
+ tp=tl;
+ tp_index=tl_index;
+ 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--;
+ 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)<<k;k+=8;
+ }
+
+ t= b&md;
+ tp=td;
+ tp_index=td_index;
+ tp_index_t_3=(tp_index+t)*3;
+ e = tp[tp_index_t_3];
+
+ do {
+
+ b>>=(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)<<k;k+=8;
+ }
+
+ d = tp[tp_index_t_3+2] + (b&inflate_mask[e]);
+
+ b>>=(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)<c?k>>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)<c?k>>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)<c?k>>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)<c?k>>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<z;j+=f){
+ System.arraycopy(r, 0, hp, (q+j)*3, 3);
+ }
+
+ // backwards increment the k-bit code i
+ for (j = 1 << (k - 1); (i & j)!=0; 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.length<vsize){ v=new int[vsize]; }
+ for(int i=0; i<vsize; i++){v[i]=0;}
+ for(int i=0; i<BMAX+1; i++){c[i]=0;}
+ for(int i=0; i<3; i++){r[i]=0;}
+// for(int i=0; i<BMAX; i++){u[i]=0;}
+ System.arraycopy(c, 0, u, 0, BMAX);
+// for(int i=0; i<BMAX+1; i++){x[i]=0;}
+ System.arraycopy(c, 0, x, 0, BMAX+1);
+ }
+}
diff --git a/app/src/main/java/com/jcraft/jzlib/Inflate.java b/app/src/main/java/com/jcraft/jzlib/Inflate.java
new file mode 100644
index 0000000..30310f6
--- /dev/null
+++ b/app/src/main/java/com/jcraft/jzlib/Inflate.java
@@ -0,0 +1,374 @@
+/* -*-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 Inflate{
+
+ static final private int MAX_WBITS=15; // 32K LZ77 window
+
+ // preset dictionary flag in zlib header
+ static final private int PRESET_DICT=0x20;
+
+ static final int Z_NO_FLUSH=0;
+ static final int Z_PARTIAL_FLUSH=1;
+ static final int Z_SYNC_FLUSH=2;
+ static final int Z_FULL_FLUSH=3;
+ static final int Z_FINISH=4;
+
+ static final private int Z_DEFLATED=8;
+
+ 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 METHOD=0; // waiting for method byte
+ static final private int FLAG=1; // waiting for flag byte
+ static final private int DICT4=2; // four dictionary check bytes to go
+ static final private int DICT3=3; // three dictionary check bytes to go
+ static final private int DICT2=4; // two dictionary check bytes to go
+ static final private int DICT1=5; // one dictionary check byte to go
+ static final private int DICT0=6; // waiting for inflateSetDictionary
+ static final private int BLOCKS=7; // decompressing blocks
+ static final private int CHECK4=8; // four check bytes to go
+ static final private int CHECK3=9; // three check bytes to go
+ static final private int CHECK2=10; // two check bytes to go
+ static final private int CHECK1=11; // one check byte to go
+ static final private int DONE=12; // finished check, done
+ static final private int BAD=13; // got an error--stay here
+
+ int mode; // current inflate mode
+
+ // mode dependent information
+ int method; // if FLAGS, method byte
+
+ // if CHECK, check values to compare
+ long[] was=new long[1] ; // computed check value
+ long need; // stream check value
+
+ // if BAD, inflateSync's marker bytes count
+ int marker;
+
+ // mode independent information
+ int nowrap; // flag for no wrapper
+ int wbits; // log2(window size) (8..15, defaults to 15)
+
+ InfBlocks blocks; // current inflate_blocks state
+
+ int inflateReset(ZStream z){
+ if(z == null || z.istate == null) return Z_STREAM_ERROR;
+
+ z.total_in = z.total_out = 0;
+ z.msg = null;
+ z.istate.mode = z.istate.nowrap!=0 ? BLOCKS : METHOD;
+ z.istate.blocks.reset(z, null);
+ return Z_OK;
+ }
+
+ int inflateEnd(ZStream z){
+ if(blocks != null)
+ blocks.free(z);
+ blocks=null;
+ // ZFREE(z, z->state);
+ 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<<w);
+
+ // reset state
+ inflateReset(z);
+ return Z_OK;
+ }
+
+ int inflate(ZStream z, int f){
+ int r;
+ int b;
+
+ if(z == null || z.istate == null || z.next_in == null)
+ return Z_STREAM_ERROR;
+ f = f == Z_FINISH ? Z_BUF_ERROR : Z_OK;
+ r = Z_BUF_ERROR;
+ while (true){
+//System.out.println("mode: "+z.istate.mode);
+ switch (z.istate.mode){
+ case METHOD:
+
+ if(z.avail_in==0)return r;r=f;
+
+ z.avail_in--; z.total_in++;
+ if(((z.istate.method = z.next_in[z.next_in_index++])&0xf)!=Z_DEFLATED){
+ z.istate.mode = BAD;
+ z.msg="unknown compression method";
+ z.istate.marker = 5; // can't try inflateSync
+ break;
+ }
+ if((z.istate.method>>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<<z.istate.wbits)){
+ length = (1<<z.istate.wbits)-1;
+ index=dictLength - length;
+ }
+ z.istate.blocks.set_dictionary(dictionary, index, length);
+ z.istate.mode = BLOCKS;
+ return Z_OK;
+ }
+
+ static private byte[] mark = {(byte)0, (byte)0, (byte)0xff, (byte)0xff};
+
+ int inflateSync(ZStream z){
+ int n; // number of bytes to look at
+ int p; // pointer to bytes
+ int m; // number of marker bytes found in a row
+ long r, w; // temporaries to save total_in and total_out
+
+ // set up
+ if(z == null || z.istate == null)
+ return Z_STREAM_ERROR;
+ if(z.istate.mode != BAD){
+ z.istate.mode = BAD;
+ z.istate.marker = 0;
+ }
+ if((n=z.avail_in)==0)
+ return Z_BUF_ERROR;
+ p=z.next_in_index;
+ m=z.istate.marker;
+
+ // search
+ while (n!=0 && m < 4){
+ if(z.next_in[p] == mark[m]){
+ m++;
+ }
+ else if(z.next_in[p]!=0){
+ m = 0;
+ }
+ else{
+ m = 4 - m;
+ }
+ p++; n--;
+ }
+
+ // restore
+ z.total_in += p-z.next_in_index;
+ z.next_in_index = p;
+ z.avail_in = n;
+ z.istate.marker = m;
+
+ // return no joy or set up to restart on a new block
+ if(m != 4){
+ return Z_DATA_ERROR;
+ }
+ r=z.total_in; w=z.total_out;
+ inflateReset(z);
+ z.total_in=r; z.total_out = w;
+ z.istate.mode = BLOCKS;
+ return Z_OK;
+ }
+
+ // Returns true if inflate is currently at the end of a block generated
+ // by Z_SYNC_FLUSH or Z_FULL_FLUSH. This function is used by one PPP
+ // implementation to provide an additional safety check. PPP uses Z_SYNC_FLUSH
+ // but removes the length bytes of the resulting empty stored block. When
+ // decompressing, PPP checks that at the end of input packet, inflate is
+ // waiting for these length bytes.
+ int inflateSyncPoint(ZStream z){
+ if(z == null || z.istate == null || z.istate.blocks == null)
+ return Z_STREAM_ERROR;
+ return z.istate.blocks.sync_point();
+ }
+}
diff --git a/app/src/main/java/com/jcraft/jzlib/JZlib.java b/app/src/main/java/com/jcraft/jzlib/JZlib.java
new file mode 100644
index 0000000..b84d7a1
--- /dev/null
+++ b/app/src/main/java/com/jcraft/jzlib/JZlib.java
@@ -0,0 +1,67 @@
+/* -*-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 public class JZlib{
+ private static final String version="1.0.2";
+ public static String version(){return version;}
+
+ // compression levels
+ static final public int Z_NO_COMPRESSION=0;
+ static final public int Z_BEST_SPEED=1;
+ static final public int Z_BEST_COMPRESSION=9;
+ static final public int Z_DEFAULT_COMPRESSION=(-1);
+
+ // compression strategy
+ static final public int Z_FILTERED=1;
+ static final public int Z_HUFFMAN_ONLY=2;
+ static final public int Z_DEFAULT_STRATEGY=0;
+
+ static final public int Z_NO_FLUSH=0;
+ static final public int Z_PARTIAL_FLUSH=1;
+ static final public int Z_SYNC_FLUSH=2;
+ static final public int Z_FULL_FLUSH=3;
+ static final public int Z_FINISH=4;
+
+ static final public int Z_OK=0;
+ static final public int Z_STREAM_END=1;
+ static final public int Z_NEED_DICT=2;
+ static final public int Z_ERRNO=-1;
+ static final public int Z_STREAM_ERROR=-2;
+ static final public int Z_DATA_ERROR=-3;
+ static final public int Z_MEM_ERROR=-4;
+ static final public int Z_BUF_ERROR=-5;
+ static final public int Z_VERSION_ERROR=-6;
+}
diff --git a/app/src/main/java/com/jcraft/jzlib/StaticTree.java b/app/src/main/java/com/jcraft/jzlib/StaticTree.java
new file mode 100644
index 0000000..0f7f577
--- /dev/null
+++ b/app/src/main/java/com/jcraft/jzlib/StaticTree.java
@@ -0,0 +1,149 @@
+/* -*-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 StaticTree{
+ static final private int MAX_BITS=15;
+
+ static final private int BL_CODES=19;
+ static final private int D_CODES=30;
+ static final private int LITERALS=256;
+ static final private int LENGTH_CODES=29;
+ static final private int L_CODES=(LITERALS+1+LENGTH_CODES);
+
+ // Bit length codes must not exceed MAX_BL_BITS bits
+ static final int MAX_BL_BITS=7;
+
+ static final short[] static_ltree = {
+ 12, 8, 140, 8, 76, 8, 204, 8, 44, 8,
+ 172, 8, 108, 8, 236, 8, 28, 8, 156, 8,
+ 92, 8, 220, 8, 60, 8, 188, 8, 124, 8,
+ 252, 8, 2, 8, 130, 8, 66, 8, 194, 8,
+ 34, 8, 162, 8, 98, 8, 226, 8, 18, 8,
+ 146, 8, 82, 8, 210, 8, 50, 8, 178, 8,
+ 114, 8, 242, 8, 10, 8, 138, 8, 74, 8,
+ 202, 8, 42, 8, 170, 8, 106, 8, 234, 8,
+ 26, 8, 154, 8, 90, 8, 218, 8, 58, 8,
+ 186, 8, 122, 8, 250, 8, 6, 8, 134, 8,
+ 70, 8, 198, 8, 38, 8, 166, 8, 102, 8,
+ 230, 8, 22, 8, 150, 8, 86, 8, 214, 8,
+ 54, 8, 182, 8, 118, 8, 246, 8, 14, 8,
+ 142, 8, 78, 8, 206, 8, 46, 8, 174, 8,
+ 110, 8, 238, 8, 30, 8, 158, 8, 94, 8,
+ 222, 8, 62, 8, 190, 8, 126, 8, 254, 8,
+ 1, 8, 129, 8, 65, 8, 193, 8, 33, 8,
+ 161, 8, 97, 8, 225, 8, 17, 8, 145, 8,
+ 81, 8, 209, 8, 49, 8, 177, 8, 113, 8,
+ 241, 8, 9, 8, 137, 8, 73, 8, 201, 8,
+ 41, 8, 169, 8, 105, 8, 233, 8, 25, 8,
+ 153, 8, 89, 8, 217, 8, 57, 8, 185, 8,
+ 121, 8, 249, 8, 5, 8, 133, 8, 69, 8,
+ 197, 8, 37, 8, 165, 8, 101, 8, 229, 8,
+ 21, 8, 149, 8, 85, 8, 213, 8, 53, 8,
+ 181, 8, 117, 8, 245, 8, 13, 8, 141, 8,
+ 77, 8, 205, 8, 45, 8, 173, 8, 109, 8,
+ 237, 8, 29, 8, 157, 8, 93, 8, 221, 8,
+ 61, 8, 189, 8, 125, 8, 253, 8, 19, 9,
+ 275, 9, 147, 9, 403, 9, 83, 9, 339, 9,
+ 211, 9, 467, 9, 51, 9, 307, 9, 179, 9,
+ 435, 9, 115, 9, 371, 9, 243, 9, 499, 9,
+ 11, 9, 267, 9, 139, 9, 395, 9, 75, 9,
+ 331, 9, 203, 9, 459, 9, 43, 9, 299, 9,
+ 171, 9, 427, 9, 107, 9, 363, 9, 235, 9,
+ 491, 9, 27, 9, 283, 9, 155, 9, 411, 9,
+ 91, 9, 347, 9, 219, 9, 475, 9, 59, 9,
+ 315, 9, 187, 9, 443, 9, 123, 9, 379, 9,
+ 251, 9, 507, 9, 7, 9, 263, 9, 135, 9,
+ 391, 9, 71, 9, 327, 9, 199, 9, 455, 9,
+ 39, 9, 295, 9, 167, 9, 423, 9, 103, 9,
+ 359, 9, 231, 9, 487, 9, 23, 9, 279, 9,
+ 151, 9, 407, 9, 87, 9, 343, 9, 215, 9,
+ 471, 9, 55, 9, 311, 9, 183, 9, 439, 9,
+ 119, 9, 375, 9, 247, 9, 503, 9, 15, 9,
+ 271, 9, 143, 9, 399, 9, 79, 9, 335, 9,
+ 207, 9, 463, 9, 47, 9, 303, 9, 175, 9,
+ 431, 9, 111, 9, 367, 9, 239, 9, 495, 9,
+ 31, 9, 287, 9, 159, 9, 415, 9, 95, 9,
+ 351, 9, 223, 9, 479, 9, 63, 9, 319, 9,
+ 191, 9, 447, 9, 127, 9, 383, 9, 255, 9,
+ 511, 9, 0, 7, 64, 7, 32, 7, 96, 7,
+ 16, 7, 80, 7, 48, 7, 112, 7, 8, 7,
+ 72, 7, 40, 7, 104, 7, 24, 7, 88, 7,
+ 56, 7, 120, 7, 4, 7, 68, 7, 36, 7,
+ 100, 7, 20, 7, 84, 7, 52, 7, 116, 7,
+ 3, 8, 131, 8, 67, 8, 195, 8, 35, 8,
+ 163, 8, 99, 8, 227, 8
+ };
+
+ static final short[] static_dtree = {
+ 0, 5, 16, 5, 8, 5, 24, 5, 4, 5,
+ 20, 5, 12, 5, 28, 5, 2, 5, 18, 5,
+ 10, 5, 26, 5, 6, 5, 22, 5, 14, 5,
+ 30, 5, 1, 5, 17, 5, 9, 5, 25, 5,
+ 5, 5, 21, 5, 13, 5, 29, 5, 3, 5,
+ 19, 5, 11, 5, 27, 5, 7, 5, 23, 5
+ };
+
+ static StaticTree static_l_desc =
+ new StaticTree(static_ltree, Tree.extra_lbits,
+ LITERALS+1, L_CODES, MAX_BITS);
+
+ static StaticTree static_d_desc =
+ new StaticTree(static_dtree, Tree.extra_dbits,
+ 0, D_CODES, MAX_BITS);
+
+ static StaticTree static_bl_desc =
+ new StaticTree(null, Tree.extra_blbits,
+ 0, BL_CODES, MAX_BL_BITS);
+
+ short[] static_tree; // static tree or null
+ int[] extra_bits; // extra bits for each code or null
+ int extra_base; // base index for extra_bits
+ int elems; // max number of elements in the tree
+ int max_length; // max bit length for the codes
+
+ StaticTree(short[] static_tree,
+ int[] extra_bits,
+ int extra_base,
+ int elems,
+ int max_length
+ ){
+ this.static_tree=static_tree;
+ this.extra_bits=extra_bits;
+ this.extra_base=extra_base;
+ this.elems=elems;
+ this.max_length=max_length;
+ }
+}
diff --git a/app/src/main/java/com/jcraft/jzlib/Tree.java b/app/src/main/java/com/jcraft/jzlib/Tree.java
new file mode 100644
index 0000000..8103897
--- /dev/null
+++ b/app/src/main/java/com/jcraft/jzlib/Tree.java
@@ -0,0 +1,365 @@
+/* -*-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 Tree{
+ static final private int MAX_BITS=15;
+ static final private int BL_CODES=19;
+ static final private int D_CODES=30;
+ static final private int LITERALS=256;
+ static final private int LENGTH_CODES=29;
+ static final private int L_CODES=(LITERALS+1+LENGTH_CODES);
+ static final private int HEAP_SIZE=(2*L_CODES+1);
+
+ // Bit length codes must not exceed MAX_BL_BITS bits
+ static final int MAX_BL_BITS=7;
+
+ // end of block literal code
+ static final int END_BLOCK=256;
+
+ // repeat previous bit length 3-6 times (2 bits of repeat count)
+ static final int REP_3_6=16;
+
+ // repeat a zero length 3-10 times (3 bits of repeat count)
+ static final int REPZ_3_10=17;
+
+ // repeat a zero length 11-138 times (7 bits of repeat count)
+ static final int REPZ_11_138=18;
+
+ // extra bits for each length code
+ static final int[] extra_lbits={
+ 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
+ };
+
+ // extra bits for each distance code
+ static final int[] extra_dbits={
+ 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
+ };
+
+ // extra bits for each bit length code
+ static final int[] extra_blbits={
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7
+ };
+
+ static final byte[] bl_order={
+ 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15};
+
+
+ // The lengths of the bit length codes are sent in order of decreasing
+ // probability, to avoid transmitting the lengths for unused bit
+ // length codes.
+
+ static final int Buf_size=8*2;
+
+ // see definition of array dist_code below
+ static final int DIST_CODE_LEN=512;
+
+ static final byte[] _dist_code = {
+ 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8,
+ 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+ 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+ 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+ 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 16, 17,
+ 18, 18, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22,
+ 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+ 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+ 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27,
+ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+ 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29,
+ 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29,
+ 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29,
+ 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29
+ };
+
+ static final byte[] _length_code={
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12,
+ 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16,
+ 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19,
+ 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22,
+ 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23,
+ 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26,
+ 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+ 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+ 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28
+ };
+
+ static final int[] base_length = {
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
+ 64, 80, 96, 112, 128, 160, 192, 224, 0
+ };
+
+ static final int[] base_dist = {
+ 0, 1, 2, 3, 4, 6, 8, 12, 16, 24,
+ 32, 48, 64, 96, 128, 192, 256, 384, 512, 768,
+ 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576
+ };
+
+ // Mapping from a distance to a distance code. dist is the distance - 1 and
+ // must not have side effects. _dist_code[256] and _dist_code[257] are never
+ // used.
+ static int d_code(int dist){
+ return ((dist) < 256 ? _dist_code[dist] : _dist_code[256+((dist)>>>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<HEAP_SIZE; h++){
+ n = s.heap[h];
+ bits = tree[tree[n*2+1]*2+1] + 1;
+ if (bits > 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<elems; n++) {
+ if(tree[n*2] != 0) {
+ s.heap[++s.heap_len] = max_code = n;
+ s.depth[n] = 0;
+ }
+ else{
+ tree[n*2+1] = 0;
+ }
+ }
+
+ // The pkzip format requires that at least one distance code exists,
+ // and that at least one bit should be sent even if there is only one
+ // possible code. So to avoid special checks later on we force at least
+ // two codes of non zero frequency.
+ while (s.heap_len < 2) {
+ node = s.heap[++s.heap_len] = (max_code < 2 ? ++max_code : 0);
+ tree[node*2] = 1;
+ s.depth[node] = 0;
+ s.opt_len--; if (stree!=null) s.static_len -= stree[node*2+1];
+ // node is 0 or 1 so it does not have extra bits
+ }
+ this.max_code = max_code;
+
+ // The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
+ // establish sub-heaps of increasing lengths:
+
+ for(n=s.heap_len/2;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<<MAX_BITS)-1,
+ // "inconsistent bit counts");
+ //Tracev((stderr,"\ngen_codes: max_code %d ", max_code));
+
+ for (n = 0; n <= max_code; n++) {
+ int len = tree[n*2+1];
+ if (len == 0) continue;
+ // Now reverse the bits
+ tree[n*2] = (short)(bi_reverse(next_code[len]++, len));
+ }
+ }
+
+ // Reverse the first len bits of a code, using straightforward code (a faster
+ // method would use a table)
+ // IN assertion: 1 <= len <= 15
+ static int bi_reverse(int code, // the value to invert
+ int len // its bit length
+ ){
+ int res = 0;
+ do{
+ res|=code&1;
+ code>>>=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);//(bufsize<z.avail_out ? bufsize : z.avail_out));
+ if(z.avail_in==-1) {
+ z.avail_in=0;
+ nomoreinput=true;
+ }
+ }
+ if(compress)
+ err=z.deflate(flush);
+ else
+ err=z.inflate(flush);
+ if(nomoreinput&&(err==JZlib.Z_BUF_ERROR))
+ return(-1);
+ if(err!=JZlib.Z_OK && err!=JZlib.Z_STREAM_END)
+ throw new ZStreamException((compress ? "de" : "in")+"flating: "+z.msg);
+ if((nomoreinput||err==JZlib.Z_STREAM_END)&&(z.avail_out==len))
+ return(-1);
+ }
+ while(z.avail_out==len&&err==JZlib.Z_OK);
+ //System.err.print("("+(len-z.avail_out)+")");
+ return(len-z.avail_out);
+ }
+
+ public long skip(long n) throws IOException {
+ int len=512;
+ if(n<len)
+ len=(int)n;
+ byte[] tmp=new byte[len];
+ return((long)read(tmp));
+ }
+
+ public int getFlushMode() {
+ return(flush);
+ }
+
+ public void setFlushMode(int flush) {
+ this.flush=flush;
+ }
+
+ /**
+ * 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 close() throws IOException{
+ in.close();
+ }
+}
diff --git a/app/src/main/java/com/jcraft/jzlib/ZOutputStream.java b/app/src/main/java/com/jcraft/jzlib/ZOutputStream.java
new file mode 100644
index 0000000..afee65b
--- /dev/null
+++ b/app/src/main/java/com/jcraft/jzlib/ZOutputStream.java
@@ -0,0 +1,156 @@
+/* -*-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 ZOutputStream extends OutputStream {
+
+ 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 OutputStream out;
+
+ public ZOutputStream(OutputStream out) {
+ super();
+ this.out=out;
+ z.inflateInit();
+ compress=false;
+ }
+
+ public ZOutputStream(OutputStream out, int level) {
+ this(out, level, false);
+ }
+ public ZOutputStream(OutputStream out, int level, boolean nowrap) {
+ super();
+ this.out=out;
+ z.deflateInit(level, nowrap);
+ compress=true;
+ }
+
+ public void write(int b) throws IOException {
+ buf1[0]=(byte)b;
+ write(buf1, 0, 1);
+ }
+
+ public void write(byte b[], int off, int len) throws IOException {
+ if(len==0)
+ return;
+ int err;
+ z.next_in=b;
+ z.next_in_index=off;
+ z.avail_in=len;
+ do{
+ z.next_out=buf;
+ z.next_out_index=0;
+ z.avail_out=bufsize;
+ if(compress)
+ err=z.deflate(flush);
+ else
+ err=z.inflate(flush);
+ if(err!=JZlib.Z_OK)
+ throw new ZStreamException((compress?"de":"in")+"flating: "+z.msg);
+ out.write(buf, 0, bufsize-z.avail_out);
+ }
+ while(z.avail_in>0 || 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<String,byte[]> retrieveIdentities();
+
+ /**
+ * @param key A <code>RSAPrivateKey</code> or <code>DSAPrivateKey</code>
+ * 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 <code>RSAPrivateKey</code> or <code>DSAPrivateKey</code>
+ * 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 <code>((cond & ChannelCondition.CLOSED) != 0)</code>.
+ */
+ public static final int TIMEOUT = 1;
+
+ /**
+ * The underlying SSH-2 channel, however not necessarily the whole connection,
+ * has been closed. This implies <code>EOF</code>. Note that there may still
+ * be unread stdout or stderr data in the local window, i.e, <code>STDOUT_DATA</code>
+ * or/and <code>STDERR_DATA</code> 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, <code>STDOUT_DATA</code> or/and <code>STDERR_DATA</code>
+ * 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 <code>Connection</code> is used to establish an encrypted TCP/IP
+ * connection to a SSH-2 server.
+ * <p>
+ * Typically, one
+ * <ol>
+ * <li>creates a {@link #Connection(String) Connection} object.</li>
+ * <li>calls the {@link #connect() connect()} method.</li>
+ * <li>calls some of the authentication methods (e.g.,
+ * {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()}).</li>
+ * <li>calls one or several times the {@link #openSession() openSession()}
+ * method.</li>
+ * <li>finally, one must close the connection and release resources with the
+ * {@link #close() close()} method.</li>
+ * </ol>
+ *
+ * @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<ConnectionMonitor> connectionMonitors = new Vector<ConnectionMonitor>();
+
+ /**
+ * Prepares a fresh <code>Connection</code> object which can then be used
+ * to establish a connection to the specified SSH-2 server.
+ * <p>
+ * 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 <code>Connection</code> 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).
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If the server does not accept the request (or if further
+ * authentication steps are needed), <code>false</code> is returned and
+ * one can retry either by using this or any other authentication method
+ * (use the <code>getRemainingAuthMethods</code> method to get a list of
+ * the remaining possible methods).
+ *
+ * @param user
+ * A <code>String</code> holding the username.
+ * @param pem
+ * A <code>String</code> 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 <code>null</code>.
+ *
+ * @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 <code>null</code> submethod
+ * list.
+ *
+ * @param user
+ * A <code>String</code> holding the username.
+ * @param cb
+ * An <code>InteractiveCallback</code> 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...)
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If the server does not accept the request (or if further
+ * authentication steps are needed), <code>false</code> is returned and
+ * one can retry either by using this or any other authentication method
+ * (use the <code>getRemainingAuthMethods</code> method to get a list of
+ * the remaining possible methods).
+ * <p>
+ * 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 <code>String</code> holding the username.
+ * @param submethods
+ * An array of submethod names, see
+ * draft-ietf-secsh-auth-kbdinteract-XX. May be <code>null</code>
+ * to indicate an empty list.
+ * @param cb
+ * An <code>InteractiveCallback</code> 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.
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If the server does not accept the request (or if further
+ * authentication steps are needed), <code>false</code> is returned and
+ * one can retry either by using this or any other authentication method
+ * (use the <code>getRemainingAuthMethods</code> method to get a list of
+ * the remaining possible methods).
+ * <p>
+ * Note: if this method fails, then please double-check that it is actually
+ * offered by the server (use
+ * {@link #getRemainingAuthMethods(String) getRemainingAuthMethods()}.
+ * <p>
+ * 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).
+ * <p>
+ * 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)}.
+ * <p>
+ * 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.
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If further authentication steps are needed, <code>false</code>
+ * is returned and one can retry by any other authentication method (use the
+ * <code>getRemainingAuthMethods</code> 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 =).
+ * <p>
+ * 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.
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If the server does not accept the request (or if further
+ * authentication steps are needed), <code>false</code> is returned and
+ * one can retry either by using this or any other authentication method
+ * (use the <code>getRemainingAuthMethods</code> method to get a list of
+ * the remaining possible methods).
+ * <p>
+ * 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 <code>String</code> holding the username.
+ * @param pemPrivateKey
+ * A <code>char[]</code> 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 <code>null</code>.
+ *
+ * @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 =).
+ * <p>
+ * If the authentication phase is complete, <code>true</code> will be
+ * returned. If the server does not accept the request (or if further
+ * authentication steps are needed), <code>false</code> is returned and
+ * one can retry either by using this or any other authentication method
+ * (use the <code>getRemainingAuthMethods</code> method to get a list of
+ * the remaining possible methods).
+ *
+ * @param user
+ * A <code>String</code> holding the username.
+ * @param key
+ * A <code>RSAPrivateKey</code> or <code>DSAPrivateKey</code>
+ * 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
+ * <code>authenticateWithPublicKey(String, char[], String)</code>.
+ * <p>
+ * 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 <code>String</code> holding the username.
+ * @param pemFile
+ * A <code>File</code> 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 <code>null</code>.
+ *
+ * @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
+ * <code>connect()</code> 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.)
+ * <p>
+ * You can add as many monitors as you like.
+ *
+ * @see ConnectionMonitor
+ *
+ * @param cmon
+ * An object implementing the <code>ConnectionMonitor</code>
+ * 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.
+ * <p>
+ * 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
+ * <code>verifier</code> to ask for permission to proceed. If
+ * <code>verifier</code> is <code>null</code>, 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).
+ * <p>
+ * 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).
+ * <p>
+ * 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).
+ * <p>
+ * 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).
+ * <p>
+ * 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).
+ * <p>
+ * 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
+ * <code>connect()</code> again.
+ *
+ * @param verifier
+ * An object that implements the {@link ServerHostKeyVerifier}
+ * interface. Pass <code>null</code> 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 <code>verifier</code> callback,
+ * but it will only have an effect after the
+ * <code>verifier</code> 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 <code>verifier</code> or there is problem
+ * during the initial crypto setup (e.g., the signature sent by
+ * the server is wrong).
+ * <p>
+ * In case of a timeout (either connectTimeout or kexTimeout) a
+ * SocketTimeoutException is thrown.
+ * <p>
+ * 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 <code>connect()</code> again
+ * without having called {@link #close()} first.
+ * <p>
+ * 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
+ * <code>LocalPortForwarder</code> 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).
+ * <p>
+ * 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
+ * <code>LocalPortForwarder</code> 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).
+ * <p>
+ * 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
+ * <code>LocalStreamForwarder</code> 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
+ * <code>DynamicPortForwarder</code> forwards TCP/IP connections that arrive
+ * at a local port via the secure tunnel to another host that is chosen via
+ * the SOCKS protocol.
+ * <p>
+ * 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
+ * <code>DynamicPortForwarder</code> forwards TCP/IP connections that arrive
+ * at a local port via the secure tunnel to another host that is chosen via
+ * the SOCKS protocol.
+ * <p>
+ * 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.
+ * <p>
+ * Works only after one has passed successfully the authentication step.
+ * There is no limit on the number of concurrent SCP clients.
+ * <p>
+ * 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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).
+ * <p>
+ * Note 2: the server may return method names that are not supported by this
+ * implementation.
+ * <p>
+ * After a successful authentication, this method must not be called
+ * anymore.
+ *
+ * @param user
+ * A <code>String</code> 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 <code>true</code> 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.)
+ * <p>
+ * 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 <code>String</code> 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).
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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
+ * <code>ssh-dss</code> and <code>ssh-rsa</code>. 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.
+ * <p>
+ * 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 <code>false</code>.
+ *
+ * @param enable
+ * the argument passed to the <code>Socket.setTCPNoDelay()</code>
+ * 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.
+ * <p>
+ * At the moment, only HTTP proxies are supported.
+ * <p>
+ * 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 <code>null</code>,
+ * 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()}.
+ * <p>
+ * A call of this method will block until the peer either agreed or
+ * disagreed to your request-
+ * <p>
+ * Note 1: this method typically fails if you
+ * <ul>
+ * <li>pass a port number for which the used remote user has not enough
+ * permissions (i.e., port &lt; 1024)</li>
+ * <li>or pass a port number that is already in use on the remote server</li>
+ * <li>or if remote port forwarding is disabled on the server.</li>
+ * </ul>
+ * <p>
+ * 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 <b>GatewayPorts</b> option is enabled
+ * (see sshd_config(5)).
+ *
+ * @param bindAddress
+ * address to bind to on the server:
+ * <ul>
+ * <li>"" means that connections are to be accepted on all
+ * protocol families supported by the SSH implementation</li>
+ * <li>"0.0.0.0" means to listen on all IPv4 addresses</li>
+ * <li>"::" means to listen on all IPv6 addresses</li>
+ * <li>"localhost" means to listen on all protocol families
+ * supported by the SSH implementation on loopback addresses
+ * only, [RFC3330] and RFC3513]</li>
+ * <li>"127.0.0.1" and "::1" indicate listening on the loopback
+ * interfaces for IPv4 and IPv6 respectively</li>
+ * </ul>
+ * @param bindPort
+ * port number to bind on the server (must be &gt; 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.
+ * <p>
+ * 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. <b>Only do this when requested by Trilead
+ * support.</b>
+ * <p>
+ * 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 <code>connect()</code> call.
+ *
+ * @param enable
+ * on/off
+ * @param logger
+ * a {@link DebugLogger DebugLogger} instance, <code>null</code>
+ * means logging using the simple logger which logs all messages
+ * to to stderr. Ignored if enabled is <code>false</code>
+ */
+ 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.
+ * <p>
+ * When this method throws an exception, then you can assume that the
+ * connection should be abandoned.
+ * <p>
+ * Note: Works only after one has passed successfully the authentication
+ * step.
+ * <p>
+ * 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 <code>ConnectionMonitor</code> 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.
+ * <p>
+ * This is an experimental feature.
+ * <p>
+ * You MUST NOT make any assumption about the thread that invokes this method.
+ * <p>
+ * <b>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.</b>
+ *
+ * @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 <code>DHGexParameters</code> object can be used to specify parameters for
+ * the diffie-hellman group exchange.
+ * <p>
+ * Depending on which constructor is used, either the use of a
+ * <code>SSH_MSG_KEX_DH_GEX_REQUEST</code> or <code>SSH_MSG_KEX_DH_GEX_REQUEST_OLD</code>
+ * 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
+ * <code>SSH_MSG_KEX_DH_GEX_REQUEST_OLD</code> request.
+ * Internally, the minimum and maximum group lengths will
+ * be set to zero.
+ *
+ * @param pref_group_len has to be &gt= 1024 and &lt;= 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
+ * <code>SSH_MSG_KEX_DH_GEX_REQUEST</code> request.
+ * <p>
+ * Note: older OpenSSH servers don't understand this request, in which
+ * case you should use the {@link #DHGexParameters(int)} constructor.
+ * <p>
+ * All values have to be &gt= 1024 and &lt;= 8192. Furthermore,
+ * min_group_len &lt;= pref_group_len &lt;= 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 <code>zero</code> 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 <code>zero</code> 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 <code>DynamicPortForwarder</code> 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 <code>HTTPProxyData</code> 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, <code>null</code>, <code>null</code>)}
+ *
+ * @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, <code>null</code>, <code>null</code>, <code>null</code>)}
+ *
+ * @param proxyHost Proxy hostname.
+ * @param proxyPort Proxy port.
+ * @param proxyUser Username for basic authentication (<code>null</code> if no authentication is needed).
+ * @param proxyPass Password for basic authentication (<code>null</code> 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").
+ * <p>
+ * Please note: if you want to use basic authentication, then both <code>proxyUser</code>
+ * and <code>proxyPass</code> must be non-null.
+ * <p>
+ * Here is an example:
+ * <p>
+ * <code>
+ * new HTTPProxyData("192.168.1.1", "3128", "proxyuser", "secret", new String[] {"User-Agent: TrileadBasedClient/1.0", "X-My-Proxy-Option: something"});
+ * </code>
+ *
+ * @param proxyHost Proxy hostname.
+ * @param proxyPort Proxy port.
+ * @param proxyUser Username for basic authentication (<code>null</code> if no authentication is needed).
+ * @param proxyPass Password for basic authentication (<code>null</code> 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 <code>null</code>.
+ */
+
+ 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 <code>InteractiveCallback</code> 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).
+ * <p>
+ * 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.
+ * <p>
+ * Please refer to draft-ietf-secsh-auth-kbdinteract-XX.txt for the details.
+ * <p>
+ * 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 <code>numPrompts</code>) of Strings
+ * @param echo
+ * an array (length <code>numPrompts</code>) 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
+ * <code>numPrompts</code>.
+ */
+ 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 <code>KnownHosts</code> class is a handy tool to verify received server hostkeys
+ * based on the information in <code>known_hosts</code> files (the ones used by OpenSSH).
+ * <p>
+ * It offers basically an in-memory database for known_hosts entries, as well as some
+ * helper functions. Entries from a <code>known_hosts</code> file can be loaded at construction time.
+ * It is also possible to add more keys later (e.g., one can parse different
+ * <code>known_hosts<code> files).
+ * <p>
+ * It is a thread safe implementation, therefore, you need only to instantiate one
+ * <code>KnownHosts</code> 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<KnownHostsEntry> publicKeys = new LinkedList<KnownHostsEntry>();
+
+ 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 <code>addHostkeyToFile()</code> 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<KnownHostsEntry> 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<PublicKey> getAllKeys(String hostname)
+ {
+ Vector<PublicKey> keys = new Vector<PublicKey>();
+
+ synchronized (publicKeys)
+ {
+ Iterator<KnownHostsEntry> 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 <code>ssh-rsa</code> or <code>ssh-dss</code>)
+ * an ordered list of hostkey algorithms is returned which can be passed
+ * to <code>Connection.setServerHostKeyAlgorithms</code>.
+ *
+ * @param hostname
+ * @return <code>null</code> 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<PublicKey> 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 <code>ssh-rsa</code> or <code>ssh-dss</code>
+ * @param serverHostKey the key blob
+ * @return <ul>
+ * <li><code>HOSTKEY_IS_OK</code>: the given hostkey matches an entry for the given hostname</li>
+ * <li><code>HOSTKEY_IS_NEW</code>: no entries found for this hostname and this type of hostkey</li>
+ * <li><code>HOSTKEY_HAS_CHANGED</code>: hostname is known, but with another key of the same type
+ * (man-in-the-middle attack?)</li>
+ * </ul>
+ * @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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>LocalPortForwarder</code> 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 <code>LocalStreamForwarder</code> 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 <code>InputStream</code> 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 <code>flush</code> method of the
+ * <code>OutputStream</code>. To signal EOF, please use the
+ * <code>close</code> method of the <code>OutputStream</code>.
+ *
+ * @return An <code>OutputStream</code> 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 <code>SCPClient</code> 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.
+ * <p>
+ * This scp client is thread safe - you can download (and upload) different sets
+ * of files concurrently without any troubles. The <code>SCPClient</code> 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
+ * <code>OutputStream</code>. Please note that, to enable flexible usage
+ * of this method, the <code>OutputStream</code> 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 <code>SFTPv3Client</code> represents a SFTP (protocol version 3)
+ * client connection tunnelled over a SSH-2 connection. This is a very simple
+ * (synchronous) implementation.
+ * <p>
+ * Basically, most methods in this class map directly to one of
+ * the packet types described in draft-ietf-secsh-filexfer-02.txt.
+ * <p>
+ * Note: this is experimental code.
+ * <p>
+ * 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.
+ * <p>
+ * Notes about file names, directory names and paths, copy-pasted
+ * from the specs:
+ * <ul>
+ * <li>SFTP v3 represents file names as strings. File names are
+ * assumed to use the slash ('/') character as a directory separator.</li>
+ * <li>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).</li>
+ * <li>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.</li>
+ * <li>An empty path name is valid, and it refers to the user's default
+ * directory (usually the user's home directory).</li>
+ * </ul>
+ * <p>
+ * 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 =).
+ * <p>
+ * 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 <code>null</code> 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 (<code>null</code> 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 <b>contents</b> is not larger than
+ * <code>maxlen</code> bytes.
+ * <p>
+ * 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
+ * <code>close()</code> 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 <code>null</code> to use server defaults. Probably only
+ * the <code>uid</code>, <code>gid</code> and <code>permissions</code>
+ * (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 <code>null</code> to use server defaults. Probably only
+ * the <code>uid</code>, <code>gid</code> and <code>permissions</code>
+ * (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.
+ * <p>
+ * <ul>
+ * <li>The server will read as many bytes as it can from the file (up to <code>len</code>),
+ * and return them.</li>
+ * <li>If EOF is encountered before reading any data, <code>-1</code> is returned.
+ * <li>If an error occurs, an exception is thrown</li>.
+ * <li>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.</li>
+ * </ul>
+ *
+ * @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 &lt; len &lt;= 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 <code>EOF</code>
+ * @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 <code>len</code> &gt; 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 <code>SFTPv3DirectoryEntry</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * The recommended format for the longname field is as follows:<br>
+ * <code>-rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer</code>
+ */
+ 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 <code>SFTPv3FileAttributes</code> 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. <code>NULL</code> if not present.
+ */
+ public Long size = null;
+
+ /**
+ * The UID attribute. <code>NULL</code> if not present.
+ */
+ public Integer uid = null;
+
+ /**
+ * The GID attribute. <code>NULL</code> if not present.
+ */
+ public Integer gid = null;
+
+ /**
+ * The POSIX permissions. <code>NULL</code> if not present.
+ * <p>
+ * Here is a list:
+ * <p>
+ * <pre>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
+ * </pre>
+ */
+ public Integer permissions = null;
+
+ /**
+ * The ATIME attribute. Represented as seconds from Jan 1, 1970 in UTC.
+ * <code>NULL</code> if not present.
+ */
+ public Long atime = null;
+
+ /**
+ * The MTIME attribute. Represented as seconds from Jan 1, 1970 in UTC.
+ * <code>NULL</code> 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 <code>0177777</code>.
+ *
+ * @return <code>NULL</code> 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 <code>SFTPv3FileHandle</code>.
+ *
+ * @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 <code>SFTPv3Client</code> 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.
+ * <p>
+ * 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 (<code>ssh-rsa</code> or <code>ssh-dss</code>)
+ * @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 <code>Session</code> 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
+ * <code>requestPTY("dumb", 0, 0, 0, 0, null)</code>.
+ *
+ * @throws IOException
+ */
+ public void requestDumbPTY() throws IOException
+ {
+ requestPTY("dumb", 0, 0, 0, 0, null);
+ }
+
+ /**
+ * Basically just another wrapper for lazy people - identical to calling
+ * <code>requestPTY(term, 0, 0, 0, 0, null)</code>.
+ *
+ * @throws IOException
+ */
+ public void requestPTY(String term) throws IOException
+ {
+ requestPTY(term, 0, 0, 0, 0, null);
+ }
+
+ /**
+ * Allocate a pseudo-terminal for this session.
+ * <p>
+ * This method may only be called before a program or shell is started in
+ * this session.
+ * <p>
+ * Different aspects can be specified:
+ * <p>
+ * <ul>
+ * <li>The TERM environment variable value (e.g., vt100)</li>
+ * <li>The terminal's dimensions.</li>
+ * <li>The encoded terminal modes.</li>
+ * </ul>
+ * 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
+ * <code>terminal_modes</code>) 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 <code>null</code>)
+ * @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.
+ * <p>
+ * 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.
+ * <p>
+ * You have to supply the name and port of your X-server.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>Session</code>. 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 <code>Session</code> (otherwise this method may
+ * block, even though more data is available).
+ *
+ * @param timeout
+ * The (non-negative) timeout in <code>ms</code>. <code>0</code> means no
+ * timeout, the call may block forever.
+ * @return
+ * <ul>
+ * <li><code>0</code> if no more data will arrive.</li>
+ * <li><code>1</code> if more data is available.</li>
+ * <li><code>-1</code> if a timeout occurred.</li>
+ * </ul>
+ *
+ * @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.
+ * <p>
+ * This method returns as soon as one of the following happens:
+ * <ul>
+ * <li>at least of the specified conditions (see {@link ChannelCondition}) holds true</li>
+ * <li>timeout > 0 and a timeout occured (TIMEOUT will be set in result conditions)</a>
+ * <li>the underlying channel was closed (CLOSED will be set in result conditions)</a>
+ * </ul>
+ * <p>
+ * 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}).
+ * <p>
+ * 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 <code>Session</code> (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, <code>0</code> 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 <code<WaitForCondition</code> method).
+ *
+ * @return An <code>Integer</code> holding the exit code, or
+ * <code>null</code> 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 <code>String</code> holding the name of the signal, or
+ * <code>null</code> 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 <code>close()</code>
+ * 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 <code>StreamGobbler</code> 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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:
+ * <p>
+ * If you do not call the StreamGobbler's <code>read()</code> 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.
+ * <p>
+ * 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<String,byte[]> 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<String,byte[]> 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.
+ * <p>
+ * 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<String, X11ServerData> x11_magic_cookies = new HashMap<String, X11ServerData>();
+
+ private TransportManager tm;
+
+ private Vector<Channel> channels = new Vector<Channel>();
+ private int nextLocalChannel = 100;
+ private boolean shutdown = false;
+ private int globalSuccessCounter = 0;
+ private int globalFailedCounter = 0;
+
+ private HashMap<Integer, RemoteForwardingData> remoteForwardings = new HashMap<Integer, RemoteForwardingData>();
+
+ private AuthAgentCallback authAgent;
+
+ private Vector<IChannelWorkerThread> listenerThreads = new Vector<IChannelWorkerThread>();
+
+ 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> channel_copy;
+
+ synchronized (channels)
+ {
+ channel_copy = (Vector<Channel>) 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> channel_copy;
+
+ synchronized (channels)
+ {
+ channel_copy = (Vector<Channel>) 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<CompressorEntry> compressors = new Vector<CompressorEntry>();
+
+ 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.
+ * <p>
+ * For further details see: <a
+ * href="http://csrc.nist.gov/encryption/aes/">http://csrc.nist.gov/encryption/aes/
+ * </a>.
+ *
+ * This implementation is based on optimizations from Dr. Brian Gladman's paper
+ * and C code at <a
+ * href="http://fp.gladman.plus.com/cryptography_technology/rijndael/">http://fp.gladman.plus.com/cryptography_technology/rijndael/
+ * </a>
+ *
+ * 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
+ * <p>
+ * 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<CipherEntry> ciphers = new Vector<CipherEntry>();
+
+ 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.
+ * <p>
+ * 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.
+ * <p>
+ * Yes, these are the "attrib-bits", even though they have "_FLAGS_" in
+ * their name. Don't ask - I did not invent it.
+ * <p>
+ * "<i>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.</i>"
+ *
+ * @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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * "<i>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].</i>"
+ *
+ * @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.
+ * <p>
+ * "<i>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.</i>"
+ *
+ * @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:
+ * <p>
+ * TODO: This comment should be moved to the open method.
+ * <p>
+ * <ul>
+ * <li>O_RDONLY
+ * <ul><li>desired-access = READ_DATA | READ_ATTRIBUTES</li></ul>
+ * </li>
+ * </ul>
+ * <ul>
+ * <li>O_WRONLY
+ * <ul><li>desired-access = WRITE_DATA | WRITE_ATTRIBUTES</li></ul>
+ * </li>
+ * </ul>
+ * <ul>
+ * <li>O_RDWR
+ * <ul><li>desired-access = READ_DATA | READ_ATTRIBUTES | WRITE_DATA | WRITE_ATTRIBUTES</li></ul>
+ * </li>
+ * </ul>
+ * <ul>
+ * <li>O_APPEND
+ * <ul>
+ * <li>desired-access = WRITE_DATA | WRITE_ATTRIBUTES | APPEND_DATA</li>
+ * <li>flags = SSH_FXF_ACCESS_APPEND_DATA and or SSH_FXF_ACCESS_APPEND_DATA_ATOMIC</li>
+ * </ul>
+ * </li>
+ * </ul>
+ * <ul>
+ * <li>O_CREAT
+ * <ul>
+ * <li>flags = SSH_FXF_OPEN_OR_CREATE</li>
+ * </ul>
+ * </li>
+ * </ul>
+ * <ul>
+ * <li>O_TRUNC
+ * <ul>
+ * <li>flags = SSH_FXF_TRUNCATE_EXISTING</li>
+ * </ul>
+ * </li>
+ * </ul>
+ * <ul>
+ * <li>O_TRUNC|O_CREATE
+ * <ul>
+ * <li>flags = SSH_FXF_CREATE_TRUNCATE</li>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * @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.
+ * <p>
+ * 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.
+ * <p>
+ * Data MUST be written atomically so that there is no chance that
+ * multiple appenders can collide and result in data being lost.
+ * <p>
+ * 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).
+ * <p>
+ * When a file is opened with this flag, the offset field in the read
+ * and write functions is ignored.
+ * <p>
+ * 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.
+ * <p>
+ * 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.)
+ * <p>
+ * 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.
+ * <p>
+ * 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.)
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * Java ASN.1 encoding:
+ * <pre>
+ * SEQUENCE ::= {
+ * r INTEGER,
+ * s INTEGER
+ * }
+ * </pre>
+ */
+ 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<String, ECParameterSpec> CURVES = new TreeMap<String, ECParameterSpec>();
+ static {
+ CURVES.put(NISTP256, EllipticCurves.nistp256);
+ CURVES.put(NISTP384, EllipticCurves.nistp384);
+ CURVES.put(NISTP521, EllipticCurves.nistp521);
+ }
+
+ private static final Map<Integer, String> CURVE_SIZES = new TreeMap<Integer, String>();
+ static {
+ CURVE_SIZES.put(256, NISTP256);
+ CURVE_SIZES.put(384, NISTP384);
+ CURVE_SIZES.put(521, NISTP521);
+ }
+
+ private static final Map<String, String> CURVE_OIDS = new TreeMap<String, String>();
+ 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 <r,s> 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<String> HOSTKEY_ALGS = new TreeSet<String>();
+ 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<String> KEX_ALGS = new TreeSet<String>();
+ 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<byte[]> asynchronousQueue = new Vector<byte[]>();
+ 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<HandlerEntry> messageHandlers = new Vector<HandlerEntry>();
+
+ 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.
+ * <p>
+ * 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.
+ * <P>
+ * <B>Maintainer:</B> 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 <RETURN> \n */
+ private byte[] crlf = new byte[2];
+ /** the data sent on pressing <LineFeed> \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 <xx> 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 <code>)
+ */
+ 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 &lt;type&gt; &lt;bytes&gt; IAC SE
+ * @param type type of SB
+ * @param sbata byte array as &lt;bytes&gt;
+ */
+ 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;j<crlf.length;j++)
+ nbuf[nbufptr++]=crlf[j];
+ break;
+ } else {
+ // copy verbatim in binary mode.
+ nbuf[nbufptr++]=buf[i];
+ }
+ break;
+ // carriage return is CR NUL */
+ case 13: // \r
+ if (receivedDX[TELOPT_BINARY + 128 ] != DO) {
+ while (nbuf.length - nbufptr < cr.length) {
+ xbuf = new byte[nbuf.length*2];
+ System.arraycopy(nbuf,0,xbuf,0,nbufptr);
+ nbuf = xbuf;
+ }
+ for (int j=0;j<cr.length;j++)
+ nbuf[nbufptr++]=cr[j];
+ } else {
+ // copy verbatim in binary mode.
+ nbuf[nbufptr++]=buf[i];
+ }
+ break;
+ // all other characters are just copied
+ default:
+ nbuf[nbufptr++]=buf[i];
+ break;
+ }
+ }
+ xbuf = new byte[nbufptr];
+ System.arraycopy(nbuf,0,xbuf,0,nbufptr);
+ write(xbuf);
+ }
+
+ public void setCRLF(String xcrlf) { crlf = xcrlf.getBytes(); }
+ public void setCR(String xcr) { cr = xcr.getBytes(); }
+
+ /**
+ * Handle telnet protocol negotiation. The buffer will be parsed
+ * and necessary actions are taken according to the telnet protocol.
+ * See <A HREF="RFC-Telnet-URL">RFC-Telnet</A>
+ * @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("<UNKNOWN "+b+" > ");
+ 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("<UNKNOWN,"+b+">");
+ 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("<UNKNOWN,"+b+">");
+ 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("<UNKNOWN,"+b+">");
+ 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("<UNKNOWN,"+b+">");
+ 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.
+ * <P>
+ * <B>Maintainer:</B> 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.<P>
+ * $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 <space> */
+ 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 <michelsu@microsoft.com>
+ //# Lori Hoerth <lorih@microsoft.com>
+ //# 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 <space> " + 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.
+
+ <p>
+ 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.
+ <p>
+ Defaults: port = 1080.<br>
+ 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<offset+4;++i)
+ hostName+="."+(addr[i] & 0xFF);
+ return hostName;
+ }
+
+ static final String bytes2IPV6(byte[] addr,int offset){
+ //Have no idea how they look like!
+ return null;
+ }
+
+}
diff --git a/app/src/main/java/net/sourceforge/jsocks/ProxyServer.java b/app/src/main/java/net/sourceforge/jsocks/ProxyServer.java
new file mode 100644
index 0000000..225149d
--- /dev/null
+++ b/app/src/main/java/net/sourceforge/jsocks/ProxyServer.java
@@ -0,0 +1,591 @@
+package net.sourceforge.jsocks;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PushbackInputStream;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.NoRouteToHostException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import net.sourceforge.jsocks.server.ServerAuthenticator;
+
+/**
+ SOCKS4 and SOCKS5 proxy, handles both protocols simultaniously.
+ Implements all SOCKS commands, including UDP relaying.
+ <p>
+ 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.
+ <p>
+ 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.<br>
+ Zero timeout implies infinity.<br>
+ 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.<br>
+ Zero timeout implies infinity.<br>
+ Default timeout is 3 minutes.
+ */
+ public static void setAcceptTimeout(int timeout){
+ acceptTimeout = timeout;
+ }
+
+ /**
+ Sets the timeout for UDPRelay server.<br>
+ Zero timeout implies infinity.<br>
+ Default timeout is 3 minutes.
+ */
+ public static void setUDPTimeout(int timeout){
+ UDPRelayServer.setTimeout(timeout);
+ }
+
+ /**
+ Sets the size of the datagrams used in the UDPRelayServer.<br>
+ 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.<br>
+ 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. <br>
+ 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.<BR>
+ 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.
+ <p>
+ SOCKS5 protocol allows to send host address as either:
+ <ul>
+ <li> IPV4, normal 4 byte address. (10 bytes header size)
+ <li> IPV6, version 6 ip address (not supported by Java as for now).
+ 22 bytes header size.
+ <li> Host name,(7+length of the host name bytes header size).
+ </ul>
+ As with other Socks equivalents, direct addresses are handled
+ transparently, that is data will be send directly when required
+ by the proxy settings.
+ <p>
+ <b>NOTE:</b><br>
+ Unlike other SOCKS Sockets, it <b>does not</b> 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:
+ <ol>
+ <li> Given version of proxy does not support UDP_ASSOCIATE.
+ <li> Proxy can't be reached.
+ <li> Authorization fails.
+ <li> Proxy does not want to perform udp forwarding, for any reason.
+ </ol>
+ 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. <BR>
+
+ <B> NOTE: </B> DatagramPacket size should be at least 10 bytes less
+ than the systems limit.
+
+ <P>
+ 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.
+ <p>
+ 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.<BR>
+ * If the packet arrived from anywhere else it is not changed.<br>
+ * <B> NOTE: </B> 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.
+ <p>
+ 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.
+ <p>
+ 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.
+ <p>
+ 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.
+ <p>
+ 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<b.length;++i){
+ int i1 = (b[i] & 0xF0) >> 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<Integer, Authentication> authMethods = new Hashtable<Integer, Authentication>();
+ 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.
+ <p>
+ 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<Integer, Authentication>) 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<Integer> 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.
+ <p>
+ 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.
+ <P>
+ 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.
+ * <P>
+ * Using Socks package can be as easy as that:
+ *
+ * <pre><tt>
+ *
+ * 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);
+ * }
+ *
+ * </tt></pre>
+ *<P>
+ * 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:
+ <ol>
+
+ <li> Proxy settings say that address should be resolved locally, but
+ this fails.
+ <li> Proxy settings say that the host should be contacted directly but
+ host name can't be resolved.
+ </ol>
+ @throws SocksException
+ If one of the following happens:
+ <ul>
+ <li> Proxy is is null.
+ <li> Proxy settings say that the host should be contacted directly but
+ this fails.
+ <li> Socks Server can't be contacted.
+ <li> Authentication fails.
+ <li> Connection is not allowed by the SOCKS proxy.
+ <li> SOCKS proxy can't establish the connection.
+ <li> Any IO error occured.
+ <li> Any protocol error occured.
+ </ul>
+ @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.
+ <p>
+ 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.<br>
+ Zero timeout implies infinity.<br>
+ Default timeout is 3 minutes.
+ */
+
+ static public void setTimeout(int timeout){
+ iddleTimeout = timeout;
+ }
+
+
+ /**
+ Sets the size of the datagrams used in the UDPRelayServer.<br>
+ 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.
+ <p>
+ 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.
+ <p>
+ 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.
+ <p>
+ 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.
+ <p>
+ SOCKSv5 allows to have multiple authentication methods, and these methods
+ might require some kind of transformations being made on the data.
+ <p>
+ 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.
+ <p>
+ SOCKSv5 allows to have multiple authentication methods, and these methods
+ might require some kind of transformations being made on the data.
+ <p>
+ 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.
+ <p>
+ If no transformation should be done on the datagrams, this method
+ should return null.
+ <p>
+ This method is called on the object returned from the startSession
+ function.
+ */
+
+ UDPEncapsulation getUdpEncapsulation();
+
+ /**
+ This method is called when a request have been read.
+ <p>
+ Implementation should decide wether to grant request or not. Returning
+ true implies granting the request, false means request should be rejected.
+ <p>
+ 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.
+ <p>
+ 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.
+ <p>
+ Implementaions should return true if the datagram is to be forwarded, and
+ false if the datagram should be dropped.
+ <p>
+ 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.
+ <p>
+ 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 <b>not</b> do
+ any authentication.
+<P>
+<FONT size="+3" color ="FF0000"> Warning!!</font><br> Should not be
+used on machines which are not behind the firewall.
+<p>
+It is only provided to make implementing other authentication schemes
+easier.<br>
+For Example: <tt><pre>
+ 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;
+ }
+ }
+</pre></tt>
+*/
+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.
+ <p>
+ 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<num_methods;++i)
+ if(method_ids[i] == methodId){
+ found = true;
+ response[1] = (byte) methodId;
+ break;
+ }
+
+ out.write(response);
+ return found;
+ }
+}
diff --git a/app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java b/app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java
new file mode 100644
index 0000000..d61ef59
--- /dev/null
+++ b/app/src/main/java/org/apache/harmony/niochar/charset/additional/IBM437.java
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.niochar.charset.additional;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+
+/* TODO: support direct byte buffers
+import org.apache.harmony.nio.AddressUtil;
+import org.apache.harmony.niochar.CharsetProviderImpl;
+*/
+
+public class IBM437 extends Charset {
+
+ public IBM437(String csName, String[] aliases) {
+ super(csName, aliases);
+ }
+
+ public boolean contains(Charset cs) {
+ return cs.name().equalsIgnoreCase("IBM367") || cs.name().equalsIgnoreCase("IBM437") || cs.name().equalsIgnoreCase("US-ASCII") ;
+ }
+
+ public CharsetDecoder newDecoder() {
+ return new Decoder(this);
+ }
+
+ public CharsetEncoder newEncoder() {
+ return new Encoder(this);
+ }
+
+ private static final class Decoder extends CharsetDecoder{
+ private Decoder(Charset cs){
+ super(cs, 1, 1);
+
+ }
+
+ private native int nDecode(char[] array, int arrPosition, int remaining, long outAddr, int absolutePos);
+
+
+ protected CoderResult decodeLoop(ByteBuffer bb, CharBuffer cb){
+ int cbRemaining = cb.remaining();
+/* TODO: support direct byte buffers
+ if(CharsetProviderImpl.hasLoadedNatives() && bb.isDirect() && bb.hasRemaining() && cb.hasArray()){
+ int toProceed = bb.remaining();
+ int cbPos = cb.position();
+ int bbPos = bb.position();
+ boolean throwOverflow = false;
+ if( cbRemaining < toProceed ) {
+ toProceed = cbRemaining;
+ throwOverflow = true;
+ }
+ int res = nDecode(cb.array(), cb.arrayOffset()+cbPos, toProceed, AddressUtil.getDirectBufferAddress(bb), bbPos);
+ bb.position(bbPos+res);
+ cb.position(cbPos+res);
+ if(throwOverflow) return CoderResult.OVERFLOW;
+ }else{
+*/
+ if(bb.hasArray() && cb.hasArray()) {
+ int rem = bb.remaining();
+ rem = cbRemaining >= 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<bStart+rem; i++) {
+ char in = (char)(bArr[i] & 0xFF);
+ if(in >= 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<Integer> 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<String> 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<String>(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<Context> contextRef;
+
+ URLItemListener(Context context) {
+ this.contextRef = new WeakReference<Context>(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<String, String> values = new HashMap<String, String>();
+// protected Map<String, String> pubkeys = new HashMap<String, String>();
+
+ 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<String> value) {
+ throw new UnsupportedOperationException("HostEditor Prefs do not support Set<String>");
+ }
+ }
+
+
+ public Editor edit() {
+ //Log.d(this.getClass().toString(), "edit()");
+ return new Editor();
+ }
+
+ public Map<String, ?> 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<String> getStringSet(String key, Set<String> defValue) {
+ throw new ClassCastException("HostEditor Prefs do not support Set<String>");
+ }
+
+ protected List<OnSharedPreferenceChangeListener> listeners = new LinkedList<OnSharedPreferenceChangeListener>();
+
+ 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<CharSequence> pubkeyNicks = new LinkedList<CharSequence>(Arrays.asList(pubkeyPref.getEntries()));
+ pubkeyNicks.addAll(pubkeydb.allValues(PubkeyDatabase.FIELD_PUBKEY_NICKNAME));
+ pubkeyPref.setEntries(pubkeyNicks.toArray(new CharSequence[pubkeyNicks.size()]));
+
+ List<CharSequence> pubkeyIds = new LinkedList<CharSequence>(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<CharSequence> charsetIdsList = new LinkedList<CharSequence>();
+ List<CharSequence> charsetNamesList = new LinkedList<CharSequence>();
+
+ for (Entry<String, Charset> 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<HostBean> 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<String> transportSelection = new ArrayAdapter<String>(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<HostBean> {
+ private List<HostBean> 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<HostBean> 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<PortForwardBean> 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<PortForwardBean> {
+ class ViewHolder {
+ public TextView nickname;
+ public TextView caption;
+ }
+
+ private List<PortForwardBean> portForwards;
+
+ public PortForwardAdapter(Context context, List<PortForwardBean> 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<PubkeyBean> 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 <code>byte[]</code>.
+ */
+ 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<String> names = new LinkedList<String>();
+ {
+ 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<PubkeyBean> {
+ private List<PubkeyBean> pubkeys;
+
+ class ViewHolder {
+ public TextView nickname;
+ public TextView caption;
+ public ImageView icon;
+ }
+
+ public PubkeyAdapter(Context context, List<PubkeyBean> 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<Void, Void, Boolean> {
+ @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<ResolveInfo> 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://<nameofpackage>.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<String, Object> entry : values.valueSet()) {
+ Object value = entry.getValue();
+ if (value != null)
+ xml.append(entry.getKey(), value);
+ }
+ xml.append(String.format("</%s>", 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<FontSizeChangedListener> fontSizeChangedListeners;
+
+ private final List<String> 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<String>();
+
+ fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
+
+ 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<String>();
+
+ fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
+
+ 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<PortForwardBean> 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<String> scanForURLs() {
+ List<String> urls = new LinkedList<String>();
+
+ 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.
+ * <p>
+ * 1st press: next key to have meta state<br />
+ * 2nd press: meta state is locked on<br />
+ * 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<TerminalBridge> bridges = new LinkedList<TerminalBridge>();
+ public Map<HostBean, WeakReference<TerminalBridge>> mHostBridgeMap =
+ new HashMap<HostBean, WeakReference<TerminalBridge>>();
+ public Map<String, WeakReference<TerminalBridge>> mNicknameBridgeMap =
+ new HashMap<String, WeakReference<TerminalBridge>>();
+
+ public TerminalBridge defaultBridge = null;
+
+ public List<HostBean> disconnected = new LinkedList<HostBean>();
+
+ public Handler disconnectHandler = null;
+
+ public Map<String, KeyHolder> loadedKeypairs = new HashMap<String, KeyHolder>();
+
+ 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<WeakReference<TerminalBridge>> mPendingReconnect
+ = new LinkedList<WeakReference<TerminalBridge>>();
+
+ 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<PubkeyBean> 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<TerminalBridge> wr = new WeakReference<TerminalBridge>(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<TerminalBridge> 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<TerminalBridge> 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<String,KeyHolder> 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<String,KeyHolder> 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<TerminalBridge>(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<TerminalBridge> 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
+ * <code>buffer</code> at the start of <code>offset</code> and a maximum of
+ * <code>length</code> 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<String,String> options) {
+ // do nothing
+ }
+
+ public Map<String,String> 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<PortForwardBean> 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<String, String> 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<String, String> 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<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ 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<String, KeyHolder> 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<String, String> getOptions() {
+ Map<String, String> options = new HashMap<String, String>();
+
+ options.put("compression", Boolean.toString(compression));
+
+ return options;
+ }
+
+ @Override
+ public void setOptions(Map<String, String> 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<PortForwardBean> 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<String, String> 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<String,byte[]> retrieveIdentities() {
+ Map<String,byte[]> pubKeys = new HashMap<String,byte[]>(manager.loadedKeypairs.size());
+
+ for (Entry<String,KeyHolder> 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.<br/>
+ * 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<String, String> 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<String, String> selection = new HashMap<String, String>();
+
+ 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<OnEntropyGatheredListener> 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<OnEntropyGatheredListener>();
+ }
+
+ 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 <code>_id</code> 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<HostBean> getHosts(boolean sortColors) {
+ String sortField = sortColors ? FIELD_HOST_COLOR : FIELD_HOST_NICKNAME;
+ List<HostBean> 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<HostBean> createHostBeans(Cursor c) {
+ List<HostBean> hosts = new LinkedList<HostBean>();
+
+ 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<HostBean> 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<String, String> selection) {
+ StringBuilder selectionBuilder = new StringBuilder();
+
+ Iterator<Entry<String, String>> i = selection.entrySet().iterator();
+
+ List<String> selectionValuesList = new LinkedList<String>();
+ int n = 0;
+ while (i.hasNext()) {
+ Entry<String, String> 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<PortForwardBean> getPortForwardsForHost(HostBean host) {
+ List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ 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 <code>_id</code> 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<PubkeyBean> allPubkeys() {
+ return getPubkeys(null, null);
+ }
+
+ public List<PubkeyBean> getAllStartPubkeys() {
+ return getPubkeys(FIELD_PUBKEY_STARTUP + " = 1 AND " + FIELD_PUBKEY_ENCRYPTED + " = 0", null);
+ }
+
+ private List<PubkeyBean> getPubkeys(String selection, String[] selectionArgs) {
+ SQLiteDatabase db = getReadableDatabase();
+
+ List<PubkeyBean> pubkeys = new LinkedList<PubkeyBean>();
+
+ 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 <code>_id</code> ascending.
+ */
+ public List<CharSequence> allValues(String column) {
+ List<CharSequence> list = new LinkedList<CharSequence>();
+
+ 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<String> mTableNames = new LinkedList<String>();
+ private static List<String> mIndexNames = new LinkedList<String>();
+
+ 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.<p>
+ *
+ * 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.<p>
+ *
+ * @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). <sigh>
+ }
+ }
+
+ /**
+ * 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.
+ * <P>
+ * 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.
+ * <P>
+ * 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</%s>", field,
+ binary ? new String(Base64.encode(input.getBytes())) : input, field));
+ } else if (data instanceof Integer) {
+ sb.append(String.format("<%s>%d</%s>", field, (Integer) data, field));
+ } else if (data instanceof Long) {
+ sb.append(String.format("<%s>%d</%s>", field, (Long) data, field));
+ } else if (data instanceof byte[]) {
+ sb.append(String.format("<%s>%s</%s>", field, new String(Base64.encode((byte[]) data)), field));
+ } else if (data instanceof Boolean) {
+ sb.append(String.format("<%s>%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<Object>() {
+// @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<String, ECParameterSpec> oidMap = new HashMap<String, ECParameterSpec>();
+// private static Map<ECParameterSpec, String> paramsMap = new HashMap<ECParameterSpec, String>();
+// private static Map<ECParameterSpec, String> friendlyNameMap = new HashMap<ECParameterSpec, String>();
+//
+// 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);
+// }
+//
+// // // <DEBUG>
+// // 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");
+// // // </DEBUG>
+//
+// 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);
+// }
+//
+// // // <DEBUG>
+// // 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");
+// // // </DEBUG>
+//
+// 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.
+ * <p>These specifiers extend the standard Android specifiers.</p>
+ */
+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.
+ *
+ * <p>Constant Value: "org.openintents.action.PICK_FILE"</p>
+ */
+ 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.
+ *
+ * <p>Constant Value: "org.openintents.action.PICK_DIRECTORY"</p>
+ */
+ public static final String ACTION_PICK_DIRECTORY = "org.openintents.action.PICK_DIRECTORY";
+
+ /**
+ * The title to display.
+ *
+ * <p>This is shown in the title bar of the file manager.</p>
+ *
+ * <p>Constant Value: "org.openintents.extra.TITLE"</p>
+ */
+ public static final String EXTRA_TITLE = "org.openintents.extra.TITLE";
+
+ /**
+ * The text on the button to display.
+ *
+ * <p>Depending on the use, it makes sense to set this to "Open" or "Save".</p>
+ *
+ * <p>Constant Value: "org.openintents.extra.BUTTON_TEXT"</p>
+ */
+ public static final String EXTRA_BUTTON_TEXT = "org.openintents.extra.BUTTON_TEXT";
+
+}