commit 1595e7a99c5540d31397f89eee5183b8eed75933 Author: Shikhar Bhushan Date: Sat Feb 27 15:36:58 2010 +0100 import local diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..c6955c3d --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +sshj - SSH library and client +Copyright 2010 Shikhar Bhushan + +This product includes code derived from software developed at +The Apache Software Foundation (http://www.apache.org/): + +- Apache MINA SSHD +- Apache Commons-Net diff --git a/README b/README new file mode 100644 index 00000000..5d759618 --- /dev/null +++ b/README @@ -0,0 +1,9 @@ +sshj - SSHv2 library for Java +============================== + +Required: +* slf4j + +Optional: +* bouncycastle for using high-strength ciphers and for reading openssh private key files +* jzlib for using zlib compression diff --git a/buildfile b/buildfile new file mode 100644 index 00000000..6156c6af --- /dev/null +++ b/buildfile @@ -0,0 +1,27 @@ +require 'buildr/scala' + +repositories.remote << 'http://www.ibiblio.org/maven2/' + +# Dependencies +SLF4J = 'org.slf4j:slf4j-api:jar:1.5.10' +SLF4J_LOG4J = 'org.slf4j:slf4j-log4j12:jar:1.5.10' +LOG4J = 'log4j:log4j:jar:1.2.15' +SSHD = transitive('org.apache.sshd:sshd-core:jar:0.3.0') +JCRAFT = 'com.jcraft:jzlib:jar:1.0.7' +BC = 'org.bouncycastle:bcprov-jdk16:jar:1.45' + +desc 'SSHv2 library for Java' +define 'sshj', :version=>'0.1.0', :group=>'sshj' do + + shell.using :scala + + compile.with SLF4J, LOG4J, SLF4J_LOG4J, BC, JCRAFT + + test.with SSHD, LOG4J, SLF4J, SLF4J_LOG4J + + package(:jar).exclude('**/examples/*') + package(:sources).exclude('**/examples/*') + package(:javadoc).exclude('**/examples/*') + package(:zip, :classifier=>'examples').path('examples').include(_('**/examples/*.{java,class}')) + +end diff --git a/src/main/java/examples/Exec.java b/src/main/java/examples/Exec.java new file mode 100644 index 00000000..6096d714 --- /dev/null +++ b/src/main/java/examples/Exec.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session.Command; + +/** This examples demonstrates how a remote command can be executed. */ +public class Exec { + + // static { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + // } + + public static void main(String... args) throws Exception { + SSHClient ssh = new SSHClient(); + ssh.loadKnownHosts(); + + ssh.connect("localhost"); + try { + + ssh.authPublickey(System.getProperty("user.name")); + + Command cmd = ssh.startSession().exec("ping google.com -n 1"); + + // Pipe.pipe(cmd.getInputStream(), System.out, cmd.getLocalMaxPacketSize(), false); + System.out.print(cmd.getOutputAsString()); + System.out.println("\n** exit status: " + cmd.getExitStatus()); + + } finally { + ssh.disconnect(); + } + } + +} diff --git a/src/main/java/examples/LocalPF.java b/src/main/java/examples/LocalPF.java new file mode 100644 index 00000000..0a24f3ce --- /dev/null +++ b/src/main/java/examples/LocalPF.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; + +import java.net.InetSocketAddress; + +/** + * This example demonstrates local port forwarding, i.e. when we listen on a particular address and port; and forward + * all incoming connections to SSH server which further forwards them to a specified address and port. + */ +public class LocalPF { + + // static + // { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + // } + + public static void main(String... args) throws Exception { + SSHClient ssh = new SSHClient(); + + ssh.loadKnownHosts(); + + ssh.connect("localhost"); + try { + + ssh.authPublickey(System.getProperty("user.name")); + + /* + * _We_ listen on localhost:8080 and forward all connections on to server, which then forwards it to + * google.com:80 + */ + + ssh.newLocalPortForwarder(new InetSocketAddress("localhost", 8080), "google.com", 80).listen(); + + } finally { + ssh.disconnect(); + } + } + +} diff --git a/src/main/java/examples/MTTest.java b/src/main/java/examples/MTTest.java new file mode 100644 index 00000000..3fd2e290 --- /dev/null +++ b/src/main/java/examples/MTTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; + +/** This example demonstrates uploading of a file over SCP to the SSH server. */ +public class MTTest { + + static { + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + } + + public static void main(String[] args) throws Exception { + final SSHClient ssh = new SSHClient(); + ssh.loadKnownHosts(); + ssh.connect("localhost"); + try { + ssh.authPublickey(System.getProperty("user.name")); + + new Thread() { + @Override + public void run() { + try { + Thread.sleep(1000); + // Compression = significant speedup for large file transfers on fast links + // present here to demo algorithm renegotiation - could have just put this before connect() + ssh.useCompression(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }.start(); + + ssh.newSCPFileTransfer().upload("/Users/shikhar/well", "/tmp/"); + } finally { + ssh.disconnect(); + } + } +} \ No newline at end of file diff --git a/src/main/java/examples/RemotePF.java b/src/main/java/examples/RemotePF.java new file mode 100644 index 00000000..868130f8 --- /dev/null +++ b/src/main/java/examples/RemotePF.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder.Forward; +import net.schmizz.sshj.connection.channel.forwarded.SocketForwardingConnectListener; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; + +import java.net.InetSocketAddress; + +/** + * This example demonstrates remote port forwarding i.e. when the remote host is made to listen on a specific address + * and port; and forwards us incoming connections. + */ +public class RemotePF { + + static { + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + } + + public static void main(String... args) throws Exception { + SSHClient client = new SSHClient(); + client.loadKnownHosts(); + + client.connect("localhost"); + try { + + client.authPublickey(System.getProperty("user.name")); + + /* + * We make _server_ listen on port 8080, which forwards all connections to us as a channel, and we further + * forward all such channels to google.com:80 + */ + client.getRemotePortForwarder().bind(new Forward(8080), // + new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80))); + + client.getTransport().setHeartbeatInterval(30); + // Something to hang on to so that the forwarding stays + client.getTransport().join(); + + } finally { + client.disconnect(); + } + } + +} diff --git a/src/main/java/examples/RudimentaryPTY.java b/src/main/java/examples/RudimentaryPTY.java new file mode 100644 index 00000000..9b62c5f2 --- /dev/null +++ b/src/main/java/examples/RudimentaryPTY.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Shell; +import net.schmizz.sshj.transport.verification.ConsoleHostKeyVerifier; +import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts; + +import java.io.File; +import java.io.IOException; + +/** A very rudimentary psuedo-terminal based on console I/O. */ +class RudimentaryPTY { + +// static { +// BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); +// } + + public static void main(String... args) throws IOException { + + final SSHClient ssh = new SSHClient(); + + ssh.addHostKeyVerifier(new ConsoleHostKeyVerifier(new File(OpenSSHKnownHosts.detectSSHDir(), "known_hosts"), System.console())); + + ssh.connect("localhost"); + + Shell shell = null; + + try { + + ssh.authPublickey(System.getProperty("user.name")); + + final Session session = ssh.startSession(); + session.allocateDefaultPTY(); + + shell = session.startShell(); + + new StreamCopier("stdout", shell.getInputStream(), System.out) + .bufSize(shell.getLocalMaxPacketSize()) + .start(); + + new StreamCopier("stderr", shell.getErrorStream(), System.err) + .bufSize(shell.getLocalMaxPacketSize()) + .start(); + + // Now make System.in act as stdin. To exit, hit Ctrl+D (since that results in an EOF on System.in) + // This is kinda messy because java only allows console input after you hit return + // But this is just an example... a GUI app could implement a proper PTY + StreamCopier.copy(System.in, shell.getOutputStream(), shell.getRemoteMaxPacketSize(), true); + + } finally { + + if (shell != null) + shell.close(); + + ssh.disconnect(); + } + } + +} diff --git a/src/main/java/examples/SCPDownload.java b/src/main/java/examples/SCPDownload.java new file mode 100644 index 00000000..52e3d15b --- /dev/null +++ b/src/main/java/examples/SCPDownload.java @@ -0,0 +1,43 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; + +/** This example demonstrates downloading of a file over SCP from the SSH server. */ +public class SCPDownload { + + static { + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + } + + public static void main(String[] args) throws Exception { + SSHClient ssh = new SSHClient(); + // ssh.useCompression(); // => significant speedup for large file transfers on fast links + ssh.loadKnownHosts(); + ssh.connect("localhost"); + try { + ssh.authPublickey(System.getProperty("user.name")); + ssh.newSCPFileTransfer().download("well", "/tmp/"); + } finally { + ssh.disconnect(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/examples/SCPUpload.java b/src/main/java/examples/SCPUpload.java new file mode 100644 index 00000000..c2965fb2 --- /dev/null +++ b/src/main/java/examples/SCPUpload.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; + +/** This example demonstrates uploading of a file over SCP to the SSH server. */ +public class SCPUpload { + + static { + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + } + + public static void main(String[] args) throws Exception { + SSHClient ssh = new SSHClient(); + ssh.loadKnownHosts(); + ssh.connect("localhost"); + try { + ssh.authPublickey(System.getProperty("user.name")); + + // Compression = significant speedup for large file transfers on fast links + // present here to demo algorithm renegotiation - could have just put this before connect() + ssh.useCompression(); + + ssh.newSCPFileTransfer().upload("/Users/shikhar/well", "/tmp/"); + } finally { + ssh.disconnect(); + } + } +} \ No newline at end of file diff --git a/src/main/java/examples/SFTPDownload.java b/src/main/java/examples/SFTPDownload.java new file mode 100644 index 00000000..fa1321e1 --- /dev/null +++ b/src/main/java/examples/SFTPDownload.java @@ -0,0 +1,42 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; + +/** This example demonstrates downloading of a file over SFTP from the SSH server. */ +public class SFTPDownload { + + static { + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + } + + public static void main(String[] args) throws Exception { + SSHClient ssh = new SSHClient(); + ssh.loadKnownHosts(); + ssh.connect("localhost"); + try { + ssh.authPublickey(System.getProperty("user.name")); + ssh.newSFTPClient().get("well", "/tmp/"); + } finally { + ssh.disconnect(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/examples/SFTPUpload.java b/src/main/java/examples/SFTPUpload.java new file mode 100644 index 00000000..545b3a87 --- /dev/null +++ b/src/main/java/examples/SFTPUpload.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; + +/** This example demonstrates uploading of a file over SFTP to the SSH server. */ +public class SFTPUpload { + + // static + // { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + // } + + public static void main(String[] args) throws Exception { + SSHClient ssh = new SSHClient(); + ssh.loadKnownHosts(); + ssh.connect("localhost"); + try { + ssh.authPublickey(System.getProperty("user.name")); + ssh.newSFTPClient().put("/Users/shikhar/well", "/tmp/"); + } finally { + ssh.disconnect(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/examples/X11.java b/src/main/java/examples/X11.java new file mode 100644 index 00000000..5186c2ed --- /dev/null +++ b/src/main/java/examples/X11.java @@ -0,0 +1,72 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 examples; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.connection.channel.forwarded.SocketForwardingConnectListener; + +import java.net.InetSocketAddress; + +/** This example demonstrates how forwarding X11 connections from a remote host can be accomplished. */ +public class X11 { + + // static { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + // } + + public static void main(String... args) throws Exception { + SSHClient ssh = new SSHClient(); + + // Compression makes X11 more feasible over slower connections + // ssh.useCompression(); + + ssh.loadKnownHosts(); + + /* + * NOTE: Forwarding incoming X connections to localhost:6000 only works if X is started without the + * "-nolisten tcp" option (this is usually not the default for good reason) + */ + ssh.registerX11Forwarder(new SocketForwardingConnectListener(new InetSocketAddress("localhost", 6000))); + + ssh.connect("localhost"); + try { + + ssh.authPublickey(System.getProperty("user.name")); + + Session sess = ssh.startSession(); + + /* + * It is recommendable to send a fake cookie, and in your ConnectListener when a connection comes in replace + * it with the real one. But here simply one from `xauth list` is being used. + */ + sess.reqX11Forwarding("MIT-MAGIC-COOKIE-1", "b0956167c9ad8f34c8a2788878307dc9", 0); + + Command cmd = sess.exec("mate"); + + new StreamCopier("stdout", cmd.getInputStream(), System.out).start(); + new StreamCopier("stderr", cmd.getErrorStream(), System.err).start(); + + // Wait for session & X11 channel to get closed + ssh.getConnection().join(); + + } finally { + ssh.disconnect(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/concurrent/Event.java b/src/main/java/net/schmizz/concurrent/Event.java new file mode 100644 index 00000000..a3f00510 --- /dev/null +++ b/src/main/java/net/schmizz/concurrent/Event.java @@ -0,0 +1,84 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.concurrent; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +/* + * Syntactic sugar around Future + */ + +/** + * A kind of {@link Future} that caters to boolean values. + *

+ * An event can be set, cleared, or awaited, similar to Python's {@code threading.event}. The key difference is that a + * waiter may be delivered an exception of parameterized type {@code T}. Furthermore, an event {@link #isSet()} when it + * is not {@code null} i.e. it can be either {@code true} or {@code false} when set. + * + * @see Future + */ +public class Event extends Future { + + /** + * Creates this event with given {@code name} and exception {@code chainer}. Allocates a new {@link + * java.util.concurrent.locks.Lock Lock} object for this event. + * + * @param name name of this event + * @param chainer {@link ExceptionChainer} that will be used for chaining exceptions + */ + public Event(String name, ExceptionChainer chainer) { + super(name, chainer); + } + + /** + * Creates this event with given {@code name}, exception {@code chainer}, and associated {@code lock}. + * + * @param name name of this event + * @param chainer {@link ExceptionChainer} that will be used for chaining exceptions + * @param lock lock to use + */ + public Event(String name, ExceptionChainer chainer, ReentrantLock lock) { + super(name, chainer, lock); + } + + /** Sets this event to be {@code true}. Short for {@code set(true)}. */ + public void set() { + super.set(true); + } + + /** + * Await this event to have a definite {@code true} or {@code false} value. + * + * @throws T if another thread meanwhile informs this event of an error + */ + public void await() throws T { + super.get(); + } + + /** + * Await this event to have a definite {@code true} or {@code false} value, for {@code timeout} seconds. + * + * @param timeout timeout in seconds + * @param unit + * + * @throws T if another thread meanwhile informs this event of an error, or timeout expires + */ + public void await(long timeout, TimeUnit unit) throws T { + super.get(timeout, unit); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/concurrent/ExceptionChainer.java b/src/main/java/net/schmizz/concurrent/ExceptionChainer.java new file mode 100644 index 00000000..6ca58486 --- /dev/null +++ b/src/main/java/net/schmizz/concurrent/ExceptionChainer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.concurrent; + +/** + * Chains an exception to desired type. For example:

+ *

+ *

+ * ExceptionChainer<SomeException> chainer = new ExceptionChainer<SomeException>()
+ * {
+ *     public SomeException chain(Throwable t)
+ *     {
+ *         if (t instanceof SomeException)
+ *             return (SomeException) t;
+ *         else
+ *             return new SomeExcepion(t);
+ *     }
+ * };
+ * 
+ * + * @param Throwable type + */ +public interface ExceptionChainer { + + Z chain(Throwable t); + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/concurrent/Future.java b/src/main/java/net/schmizz/concurrent/Future.java new file mode 100644 index 00000000..3148de5d --- /dev/null +++ b/src/main/java/net/schmizz/concurrent/Future.java @@ -0,0 +1,204 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.concurrent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Represents future data of the parameterized type {@code V} and allows waiting on it. An exception may also be + * delivered to a waiter, and will be of the parameterized type {@code T}. + *

+ * For atomic operations on a future, e.g. checking if a value is set and if it is not then setting it - in other words, + * Compare-And-Set type operations - the associated lock for the future should be acquired while doing so. + */ +public class Future { + + private final Logger log; + + private final ExceptionChainer chainer; + private final ReentrantLock lock; + private final Condition cond; + + private V val; + private T pendingEx; + + /** + * Creates this future with given {@code name} and exception {@code chainer}. Allocates a new {@link + * java.util.concurrent.locks.Lock lock} object for this future. + * + * @param name name of this future + * @param chainer {@link ExceptionChainer} that will be used for chaining exceptions + */ + public Future(String name, ExceptionChainer chainer) { + this(name, chainer, null); + } + + /** + * Creates this future with given {@code name}, exception {@code chainer}, and associated {@code lock}. + * + * @param name name of this future + * @param chainer {@link ExceptionChainer} that will be used for chaining exceptions + * @param lock lock to use + */ + public Future(String name, ExceptionChainer chainer, ReentrantLock lock) { + this.log = LoggerFactory.getLogger("<< " + name + " >>"); + this.chainer = chainer; + this.lock = lock == null ? new ReentrantLock() : lock; + this.cond = this.lock.newCondition(); + } + + /** + * Set this future's value to {@code val}. Any waiters will be delivered this value. + * + * @param val the value + */ + public void set(V val) { + lock(); + try { + log.debug("Setting to `{}`", val); + this.val = val; + cond.signalAll(); + } finally { + unlock(); + } + } + + /** + * Queues error that will be thrown in any waiting thread or any thread that attempts to wait on this future + * hereafter. + * + * @param e the error + */ + public void error(Exception e) { + lock(); + try { + pendingEx = chainer.chain(e); + cond.signalAll(); + } finally { + unlock(); + } + } + + /** Clears this future by setting its value and queued exception to {@code null}. */ + public void clear() { + lock(); + try { + pendingEx = null; + set(null); + } finally { + unlock(); + } + } + + /** + * Wait indefinitely for this future's value to be set. + * + * @return the value + * + * @throws T in case another thread informs the future of an error meanwhile + */ + public V get() throws T { + return get(0, TimeUnit.SECONDS); + } + + /** + * Wait for {@code timeout} seconds for this future's value to be set. + * + * @param timeout the timeout in seconds + * @param unit + * + * @return the value + * + * @throws T in case another thread informs the future of an error meanwhile, or the timeout expires + */ + public V get(long timeout, TimeUnit unit) throws T { + lock(); + try { + if (pendingEx != null) + throw pendingEx; + if (val != null) + return val; + log.debug("Awaiting"); + while (val == null && pendingEx == null) + if (timeout == 0) + cond.await(); + else if (!cond.await(timeout, unit)) + throw chainer.chain(new TimeoutException("Timeout expired")); + if (pendingEx != null) { + log.error("Woke to: {}", pendingEx.toString()); + throw pendingEx; + } + return val; + } catch (InterruptedException ie) { + throw chainer.chain(ie); + } finally { + unlock(); + } + } + + /** @return Whether this future has a value set, and no error waiting to pop. */ + public boolean isSet() { + lock(); + try { + return pendingEx == null && val != null; + } finally { + unlock(); + } + } + + /** @return Whether this future currently has an error set. */ + public boolean hasError() { + lock(); + try { + return pendingEx != null; + } finally { + unlock(); + } + } + + /** @return Whether this future has threads waiting on it. */ + public boolean hasWaiters() { + lock(); + try { + return lock.hasWaiters(cond); + } finally { + unlock(); + } + } + + /** + * Lock using the associated lock. Use as part of a {@code try-finally} construct in conjunction with {@link + * #unlock()}. + */ + public void lock() { + lock.lock(); + } + + /** + * Unlock using the associated lock. Use as part of a {@code try-finally} construct in conjunction with {@link + * #lock()}. + */ + public void unlock() { + lock.unlock(); + } + +} diff --git a/src/main/java/net/schmizz/concurrent/FutureUtils.java b/src/main/java/net/schmizz/concurrent/FutureUtils.java new file mode 100644 index 00000000..dce60e5b --- /dev/null +++ b/src/main/java/net/schmizz/concurrent/FutureUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.concurrent; + +import java.util.Collection; + +public class FutureUtils { + + public static void alertAll(Exception x, Future... futures) { + for (Future f : futures) + f.error(x); + } + + public static void alertAll(Exception x, Collection futures) { + for (Future f : futures) + f.error(x); + } + +} diff --git a/src/main/java/net/schmizz/sshj/AbstractService.java b/src/main/java/net/schmizz/sshj/AbstractService.java new file mode 100644 index 00000000..0a452c57 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/AbstractService.java @@ -0,0 +1,83 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj; + +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** An abstract class for {@link Service} that implements common or default functionality. */ +public abstract class AbstractService implements Service { + + /** Logger */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** Assigned name of this service */ + protected final String name; + /** Transport layer */ + protected final Transport trans; + /** Timeout for blocking operations */ + protected int timeout; + + public AbstractService(String name, Transport trans) { + this.name = name; + this.trans = trans; + timeout = trans.getTimeout(); + } + + public String getName() { + return name; + } + + public int getTimeout() { + return this.timeout; + } + + public void handle(Message msg, SSHPacket buf) throws SSHException { + trans.sendUnimplemented(); + } + + public void notifyError(SSHException error) { + log.debug("Was notified of {}", error.toString()); + } + + public void notifyUnimplemented(long seqNum) throws SSHException { + throw new SSHException(DisconnectReason.PROTOCOL_ERROR, "Unexpected: SSH_MSG_UNIMPLEMENTED"); + } + + public void request() throws TransportException { + final Service active = trans.getService(); + if (!equals(active)) + if (name.equals(active.getName())) + trans.setService(this); + else + trans.reqService(this); + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public void notifyDisconnect() throws SSHException { + log.debug("Was notified of disconnect"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/Config.java b/src/main/java/net/schmizz/sshj/Config.java new file mode 100644 index 00000000..a0b56b4d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/Config.java @@ -0,0 +1,145 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj; + +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.signature.Signature; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.compression.Compression; +import net.schmizz.sshj.transport.kex.KeyExchange; +import net.schmizz.sshj.transport.mac.MAC; +import net.schmizz.sshj.transport.random.Random; +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; + +import java.util.List; + +/** + * Holds configuration information and factories. Acts a container for factories of {@link KeyExchange}, {@link Cipher}, + * {@link Compression}, {@link MAC}, {@link Signature}, {@link Random}, and {@link FileKeyProvider}. + */ +public interface Config { + /** + * Retrieve the list of named factories for {@code Cipher}. + * + * @return a list of named {@code Cipher} factories + */ + List> getCipherFactories(); + + /** + * Retrieve the list of named factories for {@code Compression}. + * + * @return a list of named {@code Compression} factories + */ + List> getCompressionFactories(); + + /** + * Retrieve the list of named factories for {@code FileKeyProvider}. + * + * @return a list of named {@code FileKeyProvider} factories + */ + List> getFileKeyProviderFactories(); + + /** + * Retrieve the list of named factories for KeyExchange. + * + * @return a list of named KeyExchange factories + */ + List> getKeyExchangeFactories(); + + /** + * Retrieve the list of named factories for MAC. + * + * @return a list of named MAC factories + */ + List> getMACFactories(); + + /** + * Retrieve the {@link net.schmizz.sshj.transport.random.Random} factory. + * + * @return the {@link net.schmizz.sshj.transport.random.Random} factory + */ + Factory getRandomFactory(); + + /** + * Retrieve the list of named factories for {@link net.schmizz.sshj.signature.Signature} + * + * @return a list of named {@link net.schmizz.sshj.signature.Signature} factories + */ + List> getSignatureFactories(); + + /** + * Returns the software version information for identification during SSH connection initialization. For example, + * {@code "NET_3_0"}. + */ + String getVersion(); + + /** + * Set the named factories for {@link net.schmizz.sshj.transport.cipher.Cipher}. + * + * @param cipherFactories a list of named factories + */ + void setCipherFactories(List> cipherFactories); + + /** + * Set the named factories for {@link net.schmizz.sshj.transport.compression.Compression}. + * + * @param compressionFactories a list of named factories + */ + void setCompressionFactories(List> compressionFactories); + + /** + * Set the named factories for {@link net.schmizz.sshj.userauth.keyprovider.FileKeyProvider}. + * + * @param fileKeyProviderFactories a list of named factories + */ + void setFileKeyProviderFactories(List> fileKeyProviderFactories); + + /** + * Set the named factories for {@link net.schmizz.sshj.transport.kex.KeyExchange}. + * + * @param kexFactories a list of named factories + */ + void setKeyExchangeFactories(List> kexFactories); + + /** + * Set the named factories for {@link net.schmizz.sshj.transport.mac.MAC}. + * + * @param macFactories a list of named factories + */ + void setMACFactories(List> macFactories); + + /** + * Set the factory for {@link net.schmizz.sshj.transport.random.Random}. + * + * @param randomFactory the factory + */ + void setRandomFactory(Factory randomFactory); + + /** + * Set the named factories for {@link net.schmizz.sshj.signature.Signature}. + * + * @param signatureFactories a list of named factories + */ + void setSignatureFactories(List> signatureFactories); + + /** + * Set the software version information for identification during SSH connection initialization. For example, {@code + * "NET_3_0"}. + * + * @param version software version info + */ + void setVersion(String version); +} diff --git a/src/main/java/net/schmizz/sshj/ConfigImpl.java b/src/main/java/net/schmizz/sshj/ConfigImpl.java new file mode 100644 index 00000000..f5a0d92d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/ConfigImpl.java @@ -0,0 +1,152 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj; + +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.signature.Signature; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.compression.Compression; +import net.schmizz.sshj.transport.kex.KeyExchange; +import net.schmizz.sshj.transport.mac.MAC; +import net.schmizz.sshj.transport.random.Random; +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; + +import java.util.Arrays; +import java.util.List; + + +public class ConfigImpl implements Config { + + private String version; + + private Factory randomFactory; + + private List> kexFactories; + private List> cipherFactories; + private List> compressionFactories; + private List> macFactories; + private List> signatureFactories; + private List> fileKeyProviderFactories; + + public List> getCipherFactories() { + return cipherFactories; + } + + public List> getCompressionFactories() { + return compressionFactories; + } + + public List> getFileKeyProviderFactories() { + return fileKeyProviderFactories; + } + + public List> getKeyExchangeFactories() { + return kexFactories; + } + + public List> getMACFactories() { + return macFactories; + } + + public Factory getRandomFactory() { + return randomFactory; + } + + public List> getSignatureFactories() { + return signatureFactories; + } + + public String getVersion() { + return version; + } + + public void setCipherFactories(Factory.Named... cipherFactories) { + setCipherFactories(Arrays.>asList(cipherFactories)); + } + + public void setCipherFactories(List> cipherFactories) { + this.cipherFactories = cipherFactories; + } + + public void setCompressionFactories(Factory.Named... compressionFactories) { + setCompressionFactories(Arrays.>asList(compressionFactories)); + } + + public void setCompressionFactories(List> compressionFactories) { + this.compressionFactories = compressionFactories; + } + + public void setFileKeyProviderFactories(Factory.Named... fileKeyProviderFactories) { + setFileKeyProviderFactories(Arrays.>asList(fileKeyProviderFactories)); + } + + public void setFileKeyProviderFactories(List> fileKeyProviderFactories) { + this.fileKeyProviderFactories = fileKeyProviderFactories; + } + + public void setKeyExchangeFactories(Factory.Named... kexFactories) { + setKeyExchangeFactories(Arrays.>asList(kexFactories)); + } + + public void setKeyExchangeFactories(List> kexFactories) { + this.kexFactories = kexFactories; + } + + public void setMACFactories(Factory.Named... macFactories) { + setMACFactories(Arrays.>asList(macFactories)); + } + + public void setMACFactories(List> macFactories) { + this.macFactories = macFactories; + } + + public void setRandomFactory(Factory randomFactory) { + this.randomFactory = randomFactory; + } + + public void setSignatureFactories(Factory.Named... signatureFactories) { + setSignatureFactories(Arrays.>asList(signatureFactories)); + } + + public void setSignatureFactories(List> signatureFactories) { + this.signatureFactories = signatureFactories; + } + + public void setVersion(String version) { + this.version = version; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/DefaultConfig.java b/src/main/java/net/schmizz/sshj/DefaultConfig.java new file mode 100644 index 00000000..60b9ef49 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/DefaultConfig.java @@ -0,0 +1,168 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj; + +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.common.SecurityUtils; +import net.schmizz.sshj.signature.SignatureDSA; +import net.schmizz.sshj.signature.SignatureRSA; +import net.schmizz.sshj.transport.cipher.AES128CBC; +import net.schmizz.sshj.transport.cipher.AES128CTR; +import net.schmizz.sshj.transport.cipher.AES192CBC; +import net.schmizz.sshj.transport.cipher.AES192CTR; +import net.schmizz.sshj.transport.cipher.AES256CBC; +import net.schmizz.sshj.transport.cipher.AES256CTR; +import net.schmizz.sshj.transport.cipher.BlowfishCBC; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.cipher.TripleDESCBC; +import net.schmizz.sshj.transport.compression.NoneCompression; +import net.schmizz.sshj.transport.kex.DHG1; +import net.schmizz.sshj.transport.kex.DHG14; +import net.schmizz.sshj.transport.mac.HMACMD5; +import net.schmizz.sshj.transport.mac.HMACMD596; +import net.schmizz.sshj.transport.mac.HMACSHA1; +import net.schmizz.sshj.transport.mac.HMACSHA196; +import net.schmizz.sshj.transport.random.BouncyCastleRandom; +import net.schmizz.sshj.transport.random.JCERandom; +import net.schmizz.sshj.transport.random.SingletonRandomFactory; +import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; +import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Constructor for a {@link ConfigImpl} that is initialized as follows. Items marked with an asterisk are added to the + * config only if BouncyCastle is in the classpath. + *

+ *

  • {@link ConfigImpl#setKeyExchangeFactories Key exchange}: {@link DHG14}*, {@link DHG1}
  • {@link + * ConfigImpl#setCipherFactories Ciphers} [1]: {@link AES128CTR}, {@link AES192CTR}, {@link AES256CTR}, {@link + * AES128CBC}, {@link AES192CBC}, {@link AES256CBC}, {@link AES192CBC}, {@link TripleDESCBC}, {@link BlowfishCBC}
  • + *
  • {@link ConfigImpl#setMACFactories MAC}: {@link HMACSHA1}, {@link HMACSHA196}, {@link HMACMD5}, {@link + * HMACMD596}
  • {@link ConfigImpl#setCompressionFactories Compression}: {@link NoneCompression}
  • {@link + * ConfigImpl#setSignatureFactories Signature}: {@link SignatureRSA}, {@link SignatureDSA}
  • {@link + * ConfigImpl#setRandomFactory PRNG}: {@link BouncyCastleRandom}* or {@link JCERandom}
  • {@link + * ConfigImpl#setFileKeyProviderFactories Key file support}: {@link PKCS8KeyFile}*, {@link OpenSSHKeyFile}*
  • + *
  • {@link ConfigImpl#setVersion Client version}: {@code "NET_3_0"}
+ *

+ * [1] It is worth noting that Sun's JRE does not have the unlimited cryptography extension enabled by default. This + * prevents using ciphers with strength greater than 128. + */ +public class DefaultConfig extends ConfigImpl { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private static final String VERSION = "SSHJ_0_1"; + + public DefaultConfig() { + setVersion(VERSION); + final boolean bouncyCastleRegistered = SecurityUtils.isBouncyCastleRegistered(); + initKeyExchangeFactories(bouncyCastleRegistered); + initRandomFactory(bouncyCastleRegistered); + initFileKeyProviderFactories(bouncyCastleRegistered); + initCipherFactories(); + initCompressionFactories(); + initMACFactories(); + initSignatureFactories(); + } + + protected void initKeyExchangeFactories(boolean bouncyCastleRegistered) { + if (bouncyCastleRegistered) + setKeyExchangeFactories(new DHG14.Factory(), new DHG1.Factory()); + else + setKeyExchangeFactories(new DHG1.Factory()); + } + + protected void initRandomFactory(boolean bouncyCastleRegistered) { + setRandomFactory(new SingletonRandomFactory(bouncyCastleRegistered ? new BouncyCastleRandom.Factory() : new JCERandom.Factory())); + } + + protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) { + if (bouncyCastleRegistered) { + setFileKeyProviderFactories(new PKCS8KeyFile.Factory(), new OpenSSHKeyFile.Factory()); + } + } + + + protected void initCipherFactories() { + List> avail = new LinkedList>(Arrays.>asList( + new AES128CTR.Factory(), // + new AES192CTR.Factory(), // + new AES256CTR.Factory(), // + new AES128CBC.Factory(), // + new AES192CBC.Factory(), // + new AES256CBC.Factory(), // + new TripleDESCBC.Factory(), // + new BlowfishCBC.Factory())); + + // Ref. https://issues.apache.org/jira/browse/SSHD-24 + // "AES256 and AES192 requires unlimited cryptography extension" + for (Iterator> i = avail.iterator(); i.hasNext();) { + final Factory.Named f = i.next(); + try { + final Cipher c = f.create(); + final byte[] key = new byte[c.getBlockSize()]; + final byte[] iv = new byte[c.getIVSize()]; + c.init(Cipher.Mode.Encrypt, key, iv); + } catch (Exception e) { + log.warn("Disabling cipher `{}`: cipher strengths apparently limited by JCE policy", f.getName()); + i.remove(); + } + } + + setCipherFactories(avail); + } + + protected void initSignatureFactories() { + setSignatureFactories(new SignatureRSA.Factory(), new SignatureDSA.Factory()); + } + + protected void initMACFactories() { + setMACFactories(new HMACSHA1.Factory(), new HMACSHA196.Factory(), new HMACMD5.Factory(), + new HMACMD596.Factory()); + } + + protected void initCompressionFactories() { + setCompressionFactories(new NoneCompression.Factory()); + } + + +} diff --git a/src/main/java/net/schmizz/sshj/SSHClient.java b/src/main/java/net/schmizz/sshj/SSHClient.java new file mode 100644 index 00000000..386131dd --- /dev/null +++ b/src/main/java/net/schmizz/sshj/SSHClient.java @@ -0,0 +1,642 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj; + +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SecurityUtils; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.connection.ConnectionProtocol; +import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.connection.channel.direct.SessionChannel; +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import net.schmizz.sshj.connection.channel.forwarded.ConnectListener; +import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder; +import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder.ForwardedTCPIPChannel; +import net.schmizz.sshj.connection.channel.forwarded.X11Forwarder; +import net.schmizz.sshj.connection.channel.forwarded.X11Forwarder.X11Channel; +import net.schmizz.sshj.sftp.SFTPClient; +import net.schmizz.sshj.sftp.StatefulSFTPClient; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.transport.TransportProtocol; +import net.schmizz.sshj.transport.compression.DelayedZlibCompression; +import net.schmizz.sshj.transport.compression.NoneCompression; +import net.schmizz.sshj.transport.compression.ZlibCompression; +import net.schmizz.sshj.transport.verification.HostKeyVerifier; +import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts; +import net.schmizz.sshj.userauth.UserAuth; +import net.schmizz.sshj.userauth.UserAuthException; +import net.schmizz.sshj.userauth.UserAuthProtocol; +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; +import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper; +import net.schmizz.sshj.userauth.keyprovider.KeyProvider; +import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil; +import net.schmizz.sshj.userauth.method.AuthMethod; +import net.schmizz.sshj.userauth.method.AuthPassword; +import net.schmizz.sshj.userauth.method.AuthPublickey; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.PasswordUtils; +import net.schmizz.sshj.xfer.scp.SCPFileTransfer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.SocketAddress; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Secure SHell client API. + *

+ * Before connection is established, host key verification needs to be accounted for. This is done by {@link + * #addHostKeyVerifier(HostKeyVerifier) specifying} one or more {@link HostKeyVerifier} objects. Database of known + * hostname-key pairs in the OpenSSH {@code "known_hosts"} format can be {@link #loadKnownHosts(File) loaded} for host + * key verification. + *

+ * User authentication can be performed by any of the {@code auth*()} method. + *

+ * {@link #startSession()} caters to the most typical use case of starting a {@code session} channel and executing a + * remote command, starting a subsystem, etc. If you wish to request X11 forwarding for some session, first {@link + * #registerX11Forwarder(net.schmizz.sshj.connection.channel.forwarded.ConnectListener) register} a {@link + * net.schmizz.sshj.connection.channel.forwarded.ConnectListener} for {@code x11} channels. + *

+ * {@link #newLocalPortForwarder Local} and {@link #getRemotePortForwarder() remote} port forwarding is possible. There + * are also utility method for easily creating {@link #newSCPFileTransfer SCP} and {@link #newSFTPClient() SFTP} + * implementations. + *

+ * A simple example: + *

+ *

+ *

+ * client = new SSHClient();
+ * client.initUserKnownHosts();
+ * client.connect("hostname");
+ * try
+ * {
+ *     client.authPassword("username", "password");
+ *     client.startSession().exec("true");
+ *     client.getConnection().join();
+ * } finally
+ * {
+ *     client.disconnect();
+ * }
+ * 
+ *

+ * Where a password or passphrase is required, if you're extra-paranoid use the {@code char[]} based method. The {@code + * char[]} will be blanked out after use. + */ +public class SSHClient extends SocketClient implements SessionFactory { + + /** Default port for SSH */ + public static final int DEFAULT_PORT = 22; + + /** Logger */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + + /** Transport layer */ + protected final Transport trans; + + /** {@code ssh-userauth} service */ + protected final UserAuth auth; + + /** {@code ssh-connection} service */ + protected final ConnectionProtocol conn; + + /** Default constructor. Initializes this object using {@link DefaultConfig}. */ + public SSHClient() { + this(new DefaultConfig()); + } + + /** + * Constructor that allows specifying a {@code config} to be used. + * + * @param config {@link ConfigImpl} instance + */ + public SSHClient(Config config) { + super(DEFAULT_PORT); + this.trans = new TransportProtocol(config); + this.auth = new UserAuthProtocol(trans); + this.conn = new ConnectionProtocol(trans); + } + + /** + * Add a {@link HostKeyVerifier} which will be invoked for verifying host key during connection establishment and + * future key exchanges. + * + * @param hostKeyVerifier {@link HostKeyVerifier} instance + */ + public void addHostKeyVerifier(HostKeyVerifier hostKeyVerifier) { + trans.addHostKeyVerifier(hostKeyVerifier); + } + + /** + * Add a {@link HostKeyVerifier} that will verify any host at given {@code hostname:port} and a host key that has + * the given {@code fingerprint}, e.g. {@code "4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"} + * + * @param host the hostname / IP address + * @param port the port for which the {@code fingerprint} applies + * @param fingerprint expected fingerprint in colon-delimited format (16 octets in hex delimited by a colon) + * + * @see net.schmizz.sshj.common.SecurityUtils#getFingerprint + */ + public void addHostKeyVerifier(final String host, final int port, final String fingerprint) { + addHostKeyVerifier(new HostKeyVerifier() { + public boolean verify(String h, int p, PublicKey k) { + return host.equals(h) && port == p && SecurityUtils.getFingerprint(k).equals(fingerprint); + } + }); + } + + /** + * Authenticate {@code username} using the supplied {@code methods}. + * + * @param username user to authenticate + * @param methods one or more authentication method + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void auth(String username, AuthMethod... methods) throws UserAuthException, TransportException { + assert isConnected(); + auth(username, Arrays.asList(methods)); + } + + /** + * Authenticate {@code username} using the supplied {@code methods}. + * + * @param username user to authenticate + * @param methods one or more authentication method + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void auth(String username, Iterable methods) throws UserAuthException, TransportException { + assert isConnected(); + auth.authenticate(username, conn, methods); + } + + /** + * Authenticate {@code username} using the {@code "password"} authentication method. The {@code password} array is + * blanked out after use. + * + * @param username user to authenticate + * @param password the password to use for authentication + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPassword(String username, char[] password) throws UserAuthException, TransportException { + authPassword(username, PasswordUtils.createOneOff(password)); + } + + /** + * Authenticate {@code username} using the {@code "password"} authentication method. + * + * @param username user to authenticate + * @param pfinder the {@link net.schmizz.sshj.userauth.password.PasswordFinder} to use for authentication + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPassword(String username, PasswordFinder pfinder) throws UserAuthException, TransportException { + auth(username, new AuthPassword(pfinder)); + } + + /** + * Authenticate {@code username} using the {@code "password"} authentication method. + * + * @param username user to authenticate + * @param password the password to use for authentication + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPassword(String username, String password) throws UserAuthException, TransportException { + authPassword(username, password.toCharArray()); + } + + /** + * Authenticate {@code username} using the {@code "publickey"} authentication method, with keys from some commons + * locations on the file system. This method relies on {@code ~/.ssh/id_rsa} and {@code ~/.ssh/id_dsa}. + *

+ * This method does not provide a way to specify a passphrase. + * + * @param username user to authenticate + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPublickey(String username) throws UserAuthException, TransportException { + String base = System.getProperty("user.home") + File.separator + ".ssh" + File.separator; + authPublickey(username, base + "id_rsa", base + "id_dsa"); + } + + /** + * Authenticate {@code username} using the {@code "publickey"} authentication method. + *

+ * {@link KeyProvider} instances can be created using any of the of the {@code loadKeys()} method provided in this + * class. In case multiple {@code keyProviders} are specified; authentication is attempted in order as long as the + * {@code "publickey"} authentication method is available. + * + * @param username user to authenticate + * @param keyProviders one or more {@link KeyProvider} instances + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPublickey(String username, Iterable keyProviders) throws UserAuthException, + TransportException { + List am = new LinkedList(); + for (KeyProvider kp : keyProviders) + am.add(new AuthPublickey(kp)); + auth(username, am); + } + + /** + * Authenticate {@code username} using the {@code "publickey"} authentication method. + *

+ * {@link KeyProvider} instances can be created using any of the {@code loadKeys()} method provided in this class. + * In case multiple {@code keyProviders} are specified; authentication is attempted in order as long as the {@code + * "publickey"} authentication method is available. + * + * @param username user to authenticate + * @param keyProviders one or more {@link KeyProvider} instances + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPublickey(String username, KeyProvider... keyProviders) throws UserAuthException, + TransportException { + authPublickey(username, Arrays.asList(keyProviders)); + } + + /** + * Authenticate {@code username} using the {@code "publickey"} authentication method, with keys from one or more + * {@code locations} in the file system. + *

+ * In case multiple {@code locations} are specified; authentication is attempted in order as long as the {@code + * "publickey"} authentication method is available. If there is an error loading keys from any of them (e.g. file + * could not be read, file format not recognized) that key file it is ignored. + *

+ * This method does not provide a way to specify a passphrase. + * + * @param username user to authenticate + * @param locations one or more locations in the file system containing the private key + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + public void authPublickey(String username, String... locations) throws UserAuthException, TransportException { + List keyProviders = new LinkedList(); + for (String loc : locations) + try { + log.debug("Attempting to load key from: {}", loc); + keyProviders.add(loadKeys(loc)); + } catch (IOException logged) { + log.warn("Could not load keys due to: {}", logged); + } + authPublickey(username, keyProviders); + } + + /** + * Disconnects from the connected SSH server. {@code SSHClient} objects are not reusable therefore it is incorrect + * to attempt connection after this method has been called. + *

+ * This method should be called from a {@code finally} construct after connection is established; so that proper + * cleanup is done and the thread spawned by the transport layer for dealing with incoming packets is stopped. + */ + @Override + public void disconnect() throws IOException { + assert isConnected(); + trans.disconnect(); + super.disconnect(); + assert !isConnected(); + } + + /** @return associated {@link Connection} instance. */ + public Connection getConnection() { + return conn; + } + + /** @return a {@link RemotePortForwarder} that allows requesting remote forwarding over this connection. */ + public RemotePortForwarder getRemotePortForwarder() { + synchronized (conn) { + RemotePortForwarder rpf = (RemotePortForwarder) conn.get(ForwardedTCPIPChannel.TYPE); + if (rpf == null) + conn.attach(rpf = new RemotePortForwarder(conn)); + return rpf; + } + } + + /** @return the associated {@link Transport} instance. */ + public Transport getTransport() { + return trans; + } + + /** + * @return the associated {@link UserAuth} instance. This allows access to information like the {@link + * UserAuth#getBanner() authentication banner}, whether authentication was at least {@link + * UserAuth#hadPartialSuccess() partially successful}, and any {@link UserAuth#getSavedExceptions() saved + * exceptions} that were ignored because there were more authentication method that could be tried. + */ + public UserAuth getUserAuth() { + return auth; + } + + /** @return whether authenticated. */ + public boolean isAuthenticated() { + return trans.isAuthenticated(); + } + + /** @return whether connected. */ + @Override + public boolean isConnected() { + return super.isConnected() && trans.isRunning(); + } + + /** + * Creates a {@link KeyProvider} from supplied {@link KeyPair}. + * + * @param kp the key pair + * + * @return the key provider ready for use in authentication + */ + public KeyProvider loadKeys(KeyPair kp) { + return new KeyPairWrapper(kp); + } + + /** + * Returns a {@link KeyProvider} instance created from a location on the file system where an unencrypted + * private key file (does not require a passphrase) can be found. Simply calls {@link #loadKeys(String, + * PasswordFinder)} with the {@link net.schmizz.sshj.userauth.password.PasswordFinder} argument as {@code null}. + * + * @param location the location for the key file + * + * @return the key provider ready for use in authentication + * + * @throws SSHException if there was no suitable key provider available for the file format; typically because + * BouncyCastle is not in the classpath + * @throws IOException if the key file format is not known, if the file could not be read, etc. + */ + public KeyProvider loadKeys(String location) throws IOException { + return loadKeys(location, (PasswordFinder) null); + } + + /** + * Utility function for createing a {@link KeyProvider} instance from given location on the file system. Creates a + * one-off {@link PasswordFinder} using {@link net.schmizz.sshj.userauth.password.PasswordUtils#createOneOff(char[])}, + * and calls {@link #loadKeys(String,PasswordFinder)}. + * + * @param location location of the key file + * @param passphrase passphrase as a char-array + * + * @return the key provider ready for use in authentication + * + * @throws net.schmizz.sshj.common.SSHException + * if there was no suitable key provider available for the file format; typically because + * BouncyCastle is not in the classpath + * @throws IOException if the key file format is not known, if the file could not be read, etc. + */ + public KeyProvider loadKeys(String location, char[] passphrase) throws IOException { + return loadKeys(location, PasswordUtils.createOneOff(passphrase)); + } + + /** + * Creates a {@link KeyProvider} instance from given location on the file system. Currently only PKCS8 format + * private key files are supported (OpenSSH uses this format). + *

+ * + * @param location the location of the key file + * @param passwordFinder the {@link PasswordFinder} that can supply the passphrase for decryption (may be {@code + * null} in case keyfile is not encrypted) + * + * @return the key provider ready for use in authentication + * + * @throws SSHException if there was no suitable key provider available for the file format; typically because + * BouncyCastle is not in the classpath + * @throws IOException if the key file format is not known, if the file could not be read, etc. + */ + public KeyProvider loadKeys(String location, PasswordFinder passwordFinder) throws IOException { + File loc = new File(location); + FileKeyProvider.Format format = KeyProviderUtil.detectKeyFileFormat(loc); + FileKeyProvider fkp = Factory.Named.Util.create(trans.getConfig().getFileKeyProviderFactories(), format + .toString()); + if (fkp == null) + throw new SSHException("No provider available for " + format + " key file"); + fkp.init(loc, passwordFinder); + return fkp; + } + + /** + * Convenience method for creating a {@link KeyProvider} instance from a {@code location} where an encrypted + * key file is located. Calls {@link #loadKeys(String, char[])} with a character array created from the supplied + * {@code passphrase} string. + * + * @param location location of the key file + * @param passphrase passphrase as a string + * + * @return the key provider for use in authentication + * + * @throws IOException if the key file format is not known, if the file could not be read etc. + */ + public KeyProvider loadKeys(String location, String passphrase) throws IOException { + return loadKeys(location, passphrase.toCharArray()); + } + + /** + * Attempts loading the user's {@code known_hosts} file from the default locations, i.e. {@code ~/.ssh/known_hosts} + * and {@code ~/.ssh/known_hosts2} on most platforms. Adds the resulting {@link OpenSSHKnownHosts} object as a host + * key verifier. + *

+ * For finer control over which file is used, see {@link #loadKnownHosts(File)}. + * + * @throws IOException if there is an error loading from both locations + */ + public void loadKnownHosts() throws IOException { + boolean loaded = false; + final File sshDir = OpenSSHKnownHosts.detectSSHDir(); + if (sshDir != null) { + for (File loc : Arrays.asList(new File(sshDir, "known_hosts"), new File(sshDir, "known_hosts2"))) { + loadKnownHosts(loc); + loaded = true; + } + } + if (!loaded) + throw new IOException("Could not load known_hosts"); + } + + /** + * Adds a {@link OpenSSHKnownHosts} object created from the specified location as a host key verifier. + * + * @param location location for {@code known_hosts} file + * + * @throws IOException if there is an error loading from any of these locations + */ + public void loadKnownHosts(File location) throws IOException { + addHostKeyVerifier(new OpenSSHKnownHosts(location)); + } + + /** + * Create a {@link LocalPortForwarder} that will listen on {@code address} and forward incoming connections to the + * server; which will further forward them to {@code host:port}. + *

+ * The returned forwarder's {@link LocalPortForwarder#listen() listen()} method should be called to actually start + * listening, this method just creates an instance. + * + * @param address defines where the {@link net.schmizz.sshj.connection.channel.direct.LocalPortForwarder} listens + * @param host hostname to which the server will forward + * @param port the port at {@code hostname} to which the server wil forward + * + * @return a {@link LocalPortForwarder} + * + * @throws IOException if there is an error opening a local server socket + */ + public LocalPortForwarder newLocalPortForwarder(SocketAddress address, String host, int port) throws IOException { + return new LocalPortForwarder(getServerSocketFactory(), conn, address, host, port); + } + + /** + * Register a {@code listener} for handling forwarded X11 channels. Without having done this, an incoming X11 + * forwarding will be summarily rejected. + *

+ * It should be clarified that multiple listeners for X11 forwarding over a single SSH connection are not supported + * (and don't make much sense). So a subsequent call to this method is only going to replace the registered {@code + * listener}. + * + * @param listener the {@link ConnectListener} that should be delegated the responsibility of handling forwarded + * {@link X11Channel} 's + * + * @return an {@link net.schmizz.sshj.connection.channel.forwarded.X11Forwarder} that allows to {@link + * X11Forwarder#stop() stop acting} on X11 requests from server + */ + public X11Forwarder registerX11Forwarder(ConnectListener listener) { + X11Forwarder x11f = new X11Forwarder(conn, listener); + conn.attach(x11f); + return x11f; + } + + /** @return instantiated {@link SCPFileTransfer} implementation. */ + public SCPFileTransfer newSCPFileTransfer() { + assert isConnected() && isAuthenticated(); + return new SCPFileTransfer(this); + } + + /** + * @return instantiated {@link SFTPClient} implementation. + * + * @throws IOException if there is an error starting the {@code sftp} subsystem + * @see StatefulSFTPClient + */ + public SFTPClient newSFTPClient() throws IOException { + assert isConnected() && isAuthenticated(); + return new SFTPClient(this); + } + + /** + * Does key re-exchange. + * + * @throws TransportException if an error occurs during key exchange + */ + public void rekey() throws TransportException { + doKex(); + } + + public Session startSession() throws ConnectionException, TransportException { + assert isConnected() && isAuthenticated(); + SessionChannel sess = new SessionChannel(conn); + sess.open(); + assert sess.isOpen(); + return sess; + } + + /** + * Adds {@code zlib} compression to preferred compression algorithms. There is no guarantee that it will be + * successfully negotiatied. + *

+ * If the client is already connected renegotiation is done; otherwise this method simply returns (and compression + * will be negotiated during connection establishment). + * + * @throws ClassNotFoundException if {@code JZlib} is not in classpath + * @throws TransportException if an error occurs during renegotiation + */ + public void useCompression() throws ClassNotFoundException, TransportException { + trans.getConfig().setCompressionFactories(Arrays.asList( + new DelayedZlibCompression.Factory(), + new ZlibCompression.Factory(), + new NoneCompression.Factory())); + if (isConnected()) + rekey(); + } + + /** On connection establishment, also initialize the SSH transport via {@link Transport#init} and {@link #doKex()}. */ + @Override + protected void onConnect() throws IOException { + super.onConnect(); + trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream()); + doKex(); + } + + /** + * Do key exchange. + * + * @throws TransportException if error during kex + */ + protected void doKex() throws TransportException { + assert trans.isRunning(); + + long start = System.currentTimeMillis(); + + try { + trans.doKex(); + } catch (TransportException te) { + trans.disconnect(DisconnectReason.KEY_EXCHANGE_FAILED); + throw te; + } + + log.info("Key exchange took {} seconds", (System.currentTimeMillis() - start) / 1000.0); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/Service.java b/src/main/java/net/schmizz/sshj/Service.java new file mode 100644 index 00000000..a319bfa1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/Service.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj; + +import net.schmizz.sshj.common.ErrorNotifiable; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacketHandler; +import net.schmizz.sshj.transport.TransportException; + +/** Represents a service running on top of the SSH {@link net.schmizz.sshj.transport.Transport transport layer}. */ +public interface Service extends SSHPacketHandler, ErrorNotifiable { + + /** @return The assigned name for this SSH service. */ + String getName(); + + /** + * Notifies this service that a {@code SSH_MSG_UNIMPLEMENTED} was received for packet with given sequence number. + * Meant to be invoked as a callback by the transport layer. + * + * @param seqNum sequence number of the packet which the server claims is unimplemented + * + * @throws SSHException if the packet is unexpected and may represent a disruption + */ + void notifyUnimplemented(long seqNum) throws SSHException; + + /** + * Request and install this service with the associated transport. Implementations should aim to make this method + * idempotent by first checking the {@link net.schmizz.sshj.transport.Transport#getService() currently active + * service}. + * + * @throws TransportException if there is an error sending the service request + */ + void request() throws TransportException; + + void notifyDisconnect() throws SSHException; + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/SocketClient.java b/src/main/java/net/schmizz/sshj/SocketClient.java new file mode 100644 index 00000000..71aac843 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/SocketClient.java @@ -0,0 +1,206 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj; + +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; +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.SocketException; + + +abstract class SocketClient { + + private final int defaultPort; + + private Socket socket; + private InputStream input; + private OutputStream output; + + private SocketFactory socketFactory = SocketFactory.getDefault(); + private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault(); + + private static final int DEFAULT_CONNECT_TIMEOUT = 0; + private int connectTimeout = DEFAULT_CONNECT_TIMEOUT; + + private int timeout = 0; + + private String hostname; + + SocketClient(int defaultPort) { + this.defaultPort = defaultPort; + } + + public void connect(InetAddress host, int port) + throws IOException { + socket = socketFactory.createSocket(); + socket.connect(new InetSocketAddress(host, port), connectTimeout); + onConnect(); + } + + public void connect(String hostname, int port) + throws SocketException, IOException { + this.hostname = hostname; + connect(InetAddress.getByName(hostname), port); + } + + public void connect(InetAddress host, int port, + InetAddress localAddr, int localPort) + throws SocketException, IOException { + socket = socketFactory.createSocket(); + socket.bind(new InetSocketAddress(localAddr, localPort)); + socket.connect(new InetSocketAddress(host, port), connectTimeout); + onConnect(); + } + + public void connect(String hostname, int port, + InetAddress localAddr, int localPort) + throws SocketException, IOException { + this.hostname = hostname; + connect(InetAddress.getByName(hostname), port, localAddr, localPort); + } + + public void connect(InetAddress host) throws SocketException, IOException { + connect(host, defaultPort); + } + + public void connect(String hostname) throws SocketException, IOException { + connect(hostname, defaultPort); + } + + public void disconnect() throws IOException { + if (socket != null) { + socket.close(); + socket = null; + } + if (input != null) { + input.close(); + input = null; + } + if (output != null) { + output.close(); + output = null; + } + input = null; + output = null; + } + + public boolean isConnected() { + return (socket != null) && socket.isConnected(); + } + + public int getLocalPort() { + return socket.getLocalPort(); + } + + + public InetAddress getLocalAddress() { + return socket.getLocalAddress(); + } + + public String getRemoteHostname() { + return hostname == null ? (hostname = getRemoteAddress().getHostName()) : hostname; + } + + public int getRemotePort() { + return socket.getPort(); + } + + public InetAddress getRemoteAddress() { + return socket.getInetAddress(); + } + + public void setSocketFactory(SocketFactory factory) { + if (factory == null) + socketFactory = SocketFactory.getDefault(); + else + socketFactory = factory; + } + + public SocketFactory getSocketFactory() { + return socketFactory; + } + + public void setServerSocketFactory(ServerSocketFactory factory) { + if (factory == null) + serverSocketFactory = ServerSocketFactory.getDefault(); + else + serverSocketFactory = factory; + } + + public ServerSocketFactory getServerSocketFactory() { + return serverSocketFactory; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public Socket getSocket() { + return socket; + } + + InputStream getInputStream() { + return input; + } + + OutputStream getOutputStream() { + return output; + } + + void onConnect() throws IOException { + socket.setSoTimeout(timeout); + input = socket.getInputStream(); + output = socket.getOutputStream(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/common/Base64.java b/src/main/java/net/schmizz/sshj/common/Base64.java new file mode 100644 index 00000000..8d206563 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/Base64.java @@ -0,0 +1,1705 @@ +package net.schmizz.sshj.common; + + +/** + *

Encodes and decodes to and from Base64 notation.

Homepage: http://iharder.net/base64. + *

Example:

String encoded = Base64.encode( myByteArray );
byte[] + * myByteArray = Base64.decode( encoded );

The options parameter, which appears in a few + * places, is used to pass several pieces of information to the encoder. In the "higher level" method such as + * encodeBytes( bytes, options ) the options parameter can be used to indicate such things as first gzipping the bytes + * before encoding them, not inserting linefeeds, and encoding using the URL-safe and Ordered dialects.

+ * Note, according to RFC3548, Section 2.1, implementations should + * not add line feeds unless explicitly told to do so. I've got Base64 set to this behavior now, although earlier + * versions broke lines by default.

The constants defined in Base64 can be OR-ed together to combine + * options, so you might make a call like this:

String encoded = Base64.encodeBytes( mybytes, + * Base64.GZIP | Base64.DO_BREAK_LINES );

to compress the data before encoding it and then making the output + * have newline characters.

Also...

String encoded = Base64.encodeBytes( crazyString.getBytes() + * );

I am placing this code in the Public Domain. Do with it as you will. This software comes with no + * guarantees or warranties but with plenty of well-wishing instead! Please visit http://iharder.net/base64 periodically to check for updates or to contribute + * improvements.

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.3 + */ +public class Base64 { + + /** + * A {@link Base64.InputStream} will read data from another java.io.InputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + + private final boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private final byte[] buffer; // Small buffer holding converted data + private final int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private final boolean breakLines; // Break lines at less than 80 characters + private final int options; // Record options used to create the stream. + // private final byte[] alphabet; // Local copies to avoid extra method calls + private final byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the java.io.InputStream from which to read data. + * + * @since 1.3 + */ + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor + + /** + * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE mode. + *

+ * Valid options: + *

+ *

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: break lines at 76 characters
+         *     (only meaningful when encoding)</i>
+         * 
+ *

+ * Example: new Base64.InputStream( in, Base64.DECODE ) + * + * @param in the java.io.InputStream from which to read data. + * @param options Specified options + * + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public InputStream(java.io.InputStream in, int options) { + + super(in); + this.options = options; // Record for later + breakLines = (options & DO_BREAK_LINES) > 0; + encode = (options & ENCODE) > 0; + bufferLength = encode ? 4 : 3; + buffer = new byte[bufferLength]; + position = -1; + lineLength = 0; + // alphabet = getAlphabet(options); + decodabet = getDecodabet(options); + } // end constructor + + /** + * Reads enough of the input stream to convert to/from Base64 and returns the next byte. + * + * @return next byte + * + * @since 1.3 + */ + @Override + public int read() throws java.io.IOException { + + // Do we need to get data? + if (position < 0) + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } else + break; // out of for loop + + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); + position = 0; + numSigBytes = 4; + } // end if: got data + else + return -1; // Must be end of stream + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do + b = in.read(); + while (b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) + break; // Reads a -1 if end of stream + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0, options); + position = 0; + } // end if: got four characters + else if (i == 0) + return -1; + else + // Must have broken out from above. + throw new java.io.IOException("Improperly padded Base64 input."); + + } // end else: decode + + // Got data? + if (position >= 0) { + // End of relevant data? + if ( /* !encode && */position >= numSigBytes) + return -1; + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) + position = -1; + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + else + throw new java.io.IOException("Error in Base64 code reading stream."); + } // end read + + /** + * Calls {@link #read()} repeatedly until the end of stream is reached or len bytes are read. Returns + * number of bytes read into array or -1 if end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * + * @return bytes read into array or -1 if end of stream is encountered. + * + * @since 1.3 + */ + @Override + public int read(byte[] dest, int off, int len) throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + if (b >= 0) + dest[off + i] = (byte) b; + else if (i == 0) + return -1; + else + break; // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + /** + * A {@link Base64.OutputStream} will write data to another java.io.OutputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private final boolean encode; + private int position; + private byte[] buffer; + private final int bufferLength; + private int lineLength; + private final boolean breakLines; + private final byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private final int options; // Record for later + // private final byte[] alphabet; // Local copies to avoid extra method calls + private final byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + /** + * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE mode. + *

+ * Valid options: + *

+ *

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)</i>
+         * 
+ *

+ * Example: new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + breakLines = (options & DO_BREAK_LINES) > 0; + encode = (options & ENCODE) > 0; + bufferLength = encode ? 3 : 4; + buffer = new byte[bufferLength]; + position = 0; + lineLength = 0; + suspendEncoding = false; + b4 = new byte[4]; + this.options = options; + // alphabet = getAlphabet(options); + decodabet = getDecodabet(options); + } // end constructor + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flush(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + /** + * Flushes the stream (and the enclosing streams). + * + * @throws java.io.IOException + * @since 2.3 + */ + @Override + public void flush() throws java.io.IOException { + flushBase64(); + super.flush(); + } + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer without closing the stream. + * + * @throws java.io.IOException if there's an error. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) + if (encode) { + out.write(encode3to4(b4, buffer, position, options)); + position = 0; + } // end if: encoding + else + throw new java.io.IOException("Base64 input not properly padded."); + + } // end flush + + /** + * Resumes encoding of the stream. May be helpful if you need to embed a piece of base64-encoded data in a + * stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + suspendEncoding = false; + } // end resumeEncoding + + /** + * Suspends encoding of the stream. May be helpful if you need to embed a piece of base64-encoded data in a + * stream. + * + * @throws java.io.IOException if there's an error flushing + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + suspendEncoding = true; + } // end suspendEncoding + + /** + * Calls {@link #write(int)} repeatedly until len bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * + * @since 1.3 + */ + @Override + public void write(byte[] theBytes, int off, int len) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) + write(theBytes[off + i]); + + } // end write + + /** + * Writes the byte to the output stream after converting to/from Base64 notation. When encoding, bytes are + * buffered three at a time before the output stream actually gets a write() call. When decoding, bytes are + * buffered four at a time. + * + * @param theByte the byte to write + * + * @since 1.3 + */ + @Override + public void write(int theByte) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theByte); + return; + } // end if: supsended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to encode. + + out.write(encode3to4(b4, buffer, bufferLength, options)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + else // Meaningful Base64 character? + if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to output. + + int len = Base64.decode4to3(buffer, 0, b4, 0, options); + out.write(b4, 0, len); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) + throw new java.io.IOException("Invalid character in Base64 data."); + } // end write + + } // end inner class OutputStream + + /** No options specified. Value is zero. */ + public final static int NO_OPTIONS = 0; + + /** Specify encoding in first bit. Value is one. */ + public final static int ENCODE = 1; + + /** Specify decoding in first bit. Value is zero. */ + public final static int DECODE = 0; + + /** Specify that data should be gzip-compressed in second bit. Value is two. */ + public final static int GZIP = 2; + + /** Do break lines when encoding. Value is 8. */ + public final static int DO_BREAK_LINES = 8; + + /* ******** P R I V A T E F I E L D S ******** */ + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. It is important to note + * that data encoded this way is not officially valid Base64, or at the very least should not be called + * Base64 without also specifying that is was encoded using the URL- and Filename-safe dialect. + */ + public final static int URL_SAFE = 16; + + /** Encode using the special "ordered" dialect of Base64 described here: http://www.faqs.org/qa/rfcc-1940.html. */ + public final static int ORDERED = 32; + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte) '\n'; + /** Preferred encoding. */ + private final static String PREFERRED_ENCODING = "US-ASCII"; + + /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private final static byte[] _STANDARD_ALPHABET = {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', + (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', + (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', + (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', + (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', + (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/'}; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a negative number indicating some other + * meaning. + */ + private final static byte[] _STANDARD_DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal + // 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. Notice that the last two + * bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private final static byte[] _URL_SAFE_ALPHABET = {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', + (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', + (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', + (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', + (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', + (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_'}; + + /** Used in decoding URL- and Filename-safe dialects of Base64. */ + private final static byte[] _URL_SAFE_DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal + // 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, and it is described here: http://www.faqs.org/qa/rfcc-1940.html. + */ + private final static byte[] _ORDERED_ALPHABET = {(byte) '-', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', + (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', + (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a', (byte) 'b', (byte) 'c', + (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', + (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z'}; + + /** Used in decoding the "ordered" dialect of Base64. */ + private final static byte[] _ORDERED_DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal + // 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + /** + * Low-level access to decoding ASCII characters in the form of a byte array. Ignores GUNZIP option, if it's + * set. This is not generally a recommended method, although it is used internally as part of the decoding + * process. Special case: if len = 0, an empty array is returned. Still, if you need more speed and reduced memory + * footprint (and aren't gzipping), consider this method. + * + * @param source The Base64 encoded data + * + * @return decoded data + * + * @since 2.3.1 + */ + public static byte[] decode(byte[] source) { + byte[] decoded = null; + try { + decoded = decode(source, 0, source.length, Base64.NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return decoded; + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Low-level access to decoding ASCII characters in the form of a byte array. Ignores GUNZIP option, if it's + * set. This is not generally a recommended method, although it is used internally as part of the decoding + * process. Special case: if len = 0, an empty array is returned. Still, if you need more speed and reduced memory + * footprint (and aren't gzipping), consider this method. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param options Can specify options such as alphabet type to use + * + * @return decoded data + * + * @throws java.io.IOException If bogus characters exist in source data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len, int options) throws java.io.IOException { + + // Lots of error checking and exception throwing + if (source == null) + throw new NullPointerException("Cannot decode null source array."); + if (off < 0 || off + len > source.length) + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, + len)); + + if (len == 0) + return new byte[0]; + else if (len < 4) + throw new IllegalArgumentException( + "Base64-encoded string must have at least four characters, but length specified was " + len); + + byte[] DECODABET = getDecodabet(options); + + int len34 = len * 3 / 4; // Estimate on array size + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; // Keep track of where we're writing + + byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space + int b4Posn = 0; // Keep track of four byte input buffer + int i = 0; // Source array counter + byte sbiCrop = 0; // Low seven bits (ASCII) of input + byte sbiDecode = 0; // Special value from DECODABET + + for (i = off; i < off + len; i++) { // Loop through source + + sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[sbiCrop]; // Special value + + // White space, Equals sign, or legit Base64 character + // Note the values such as -5 and -9 in the + // DECODABETs at the top of the file. + if (sbiDecode >= WHITE_SPACE_ENC) { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = sbiCrop; // Save non-whitespace + if (b4Posn > 3) { // Time to decode? + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (sbiCrop == EQUALS_SIGN) + break; + } // end if: quartet built + } // end if: equals sign or better + } // end if: white space, equals sign or better + else + // There's a bad input character in the Base64 stream. + throw new java.io.IOException(String.format("Bad Base64 input character '%c' in array position %d", + source[i], i)); + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * + * @return the decoded data + * + * @throws java.io.IOException If there is a problem + * @since 1.4 + */ + public static byte[] decode(String s) throws java.io.IOException { + return decode(s, NO_OPTIONS); + } + + /** + * Decodes data from Base64 notation, automatically detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * + * @return the decoded data + * + * @throws java.io.IOException if there is an error + * @throws NullPointerException if s is null + * @since 1.4 + */ + public static byte[] decode(String s, int options) throws java.io.IOException { + + if (s == null) + throw new NullPointerException("Input string was null."); + + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length, options); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + if (bytes != null && bytes.length >= 4) { + + int head = bytes[0] & 0xff | bytes[1] << 8 & 0xff00; + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) + baos.write(buffer, 0, length); + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void decodeFileToFile(String infile, String outfile) throws java.io.IOException { + + byte[] decoded = Base64.decodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outfile)); + out.write(decoded); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { + out.close(); + } catch (Exception ex) { + } + } // end finally + } // end decodeFileToFile + + /** + * Convenience method for reading a base64-encoded file and decoding it.

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, it just returned + * false, but in retrospect that's a pretty poor way to next it.

+ * + * @param filename Filename for reading encoded data + * + * @return decoded byte array + * + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) throws java.io.IOException { + + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) + throw new java.io.IOException("File is too big for this convenience method (" + file.length() + + " bytes)."); + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream(new java.io.FileInputStream(file)), + Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) + length += numBytes; + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + /** + * Convenience method for decoding data to a file.

As of v 2.3, if there is a error, the method will throw + * an java.io.IOException. This is new to v2.3! In earlier versions, it just returned false, but in + * retrospect that's a pretty poor way to next it.

+ * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static void decodeToFile(String dataToDecode, String filename) throws java.io.IOException { + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream(new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + } // end decodeToFile + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. Returns null if there was an + * error. + * + * @param encodedObject The Base64 data to decode + * + * @return The decoded and deserialized object + * + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a class that cannot be found by the JVM + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) throws java.io.IOException, + java.lang.ClassNotFoundException { + + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + ois = new java.io.ObjectInputStream(bais); + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + catch (java.lang.ClassNotFoundException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + /** + * Performs Base64 encoding on the raw ByteBuffer, writing it to the encoded ByteBuffer. + * This is an experimental feature. Currently it does not pass along any options (such as {@link #DO_BREAK_LINES} or + * {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * + * @since 2.3 + */ + public static void encode(java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while (raw.hasRemaining()) { + int rem = Math.min(3, raw.remaining()); + raw.get(raw3, 0, rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + encoded.put(enc4); + } // end input remaining + } + + /** + * Performs Base64 encoding on the raw ByteBuffer, writing it to the encoded CharBuffer. + * This is an experimental feature. Currently it does not pass along any options (such as {@link #DO_BREAK_LINES} or + * {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * + * @since 2.3 + */ + public static void encode(java.nio.ByteBuffer raw, java.nio.CharBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while (raw.hasRemaining()) { + int rem = Math.min(3, raw.remaining()); + raw.get(raw3, 0, rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + for (int i = 0; i < 4; i++) + encoded.put((char) (enc4[i] & 0xFF)); + } // end input remaining + } + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source The data to convert + * + * @return The data in Base64-encoded form + * + * @throws NullPointerException if source array is null + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation.

Example options:

+ *

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+     * 
+ *

Example: encodeBytes( myData, Base64.GZIP ) or

Example: encodeBytes( myData, + * Base64.GZIP | Base64.DO_BREAK_LINES )

As of v 2.3, if there is an error with the GZIP + * stream, the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, it just + * returned a null value, but in retrospect that's a pretty poor way to next it.

+ * + * @param source The data to convert + * @param options Specified options + * + * @return The Base64-encoded data as a String + * + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) throws java.io.IOException { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data.

As of v 2.3, if there is an + * error, the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, it just + * returned a null value, but in retrospect that's a pretty poor way to next it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * + * @return The Base64-encoded data as a String + * + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, off, len, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation.

Example options:

+ *

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
+     * 
+ *

Example: encodeBytes( myData, Base64.GZIP ) or

Example: encodeBytes( myData, + * Base64.GZIP | Base64.DO_BREAK_LINES )

As of v 2.3, if there is an error with the GZIP + * stream, the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, it just + * returned a null value, but in retrospect that's a pretty poor way to next it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * + * @return The Base64-encoded data as a String + * + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, int options) throws java.io.IOException { + byte[] encoded = encodeBytesToBytes(source, off, len, options); + + // Return value according to relevant encoding. + try { + return new String(encoded, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(encoded); + } // end catch + + } // end encodeBytes + + /** + * Similar to {@link #encodeBytes(byte[])} but returns a byte array instead of instantiating a String. This is more + * efficient if you're working with I/O streams and have large data sets to encode. + * + * @param source The data to convert + * + * @return The Base64-encoded data as a byte[] (of ASCII characters) + * + * @throws NullPointerException if source array is null + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source) { + byte[] encoded = null; + try { + encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return encoded; + } + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns a byte array instead of instantiating a + * String. This is more efficient if you're working with I/O streams and have large data sets to encode. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * + * @return The Base64-encoded data as a String + * + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) throws java.io.IOException { + + if (source == null) + throw new NullPointerException("Cannot serialize a null array."); + + if (off < 0) + throw new IllegalArgumentException("Cannot have negative offset: " + off); + + if (len < 0) + throw new IllegalArgumentException("Cannot have length offset: " + len); + + if (off + len > source.length) + throw new IllegalArgumentException(String.format( + "Cannot have offset of %d and length of %d with array of length %d", off, len, source.length)); + + // Compress? + if ((options & GZIP) > 0) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + return baos.toByteArray(); + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + boolean breakLines = (options & DO_BREAK_LINES) > 0; + + // int len43 = len * 4 / 3; + // byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = len / 3 * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding + if (breakLines) + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + byte[] outBuff = new byte[encLen]; + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e, options); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, options); + e += 4; + } // end if: some padding needed + + // Only resize array if we didn't guess it right. + if (e < outBuff.length - 1) { + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff, 0, finalOut, 0, e); + // System.err.println("Having to resize array from " + outBuff.length + " to " + e + // ); + return finalOut; + } else + // System.err.println("No need to resize array."); + return outBuff; + + } // end else: don't compress + + } // end encodeBytesToBytes + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void encodeFileToFile(String infile, String outfile) throws java.io.IOException { + + String encoded = Base64.encodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outfile)); + out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output. + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { + out.close(); + } catch (Exception ex) { + } + } // end finally + } // end encodeFileToFile + + /** + * Convenience method for reading a binary file and base64-encoding it.

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, it just returned + * false, but in retrospect that's a pretty poor way to next it.

+ * + * @param filename Filename for reading binary data + * + * @return base64-encoded string + * + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static String encodeFromFile(String filename) throws java.io.IOException { + + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = new byte[Math.max((int) (file.length() * 1.4), 40)]; // Need max() for + // math on small + // files (v2.2.1) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream(new java.io.FileInputStream(file)), + Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) + length += numBytes; + + // Save in a variable to return + encodedData = new String(buffer, 0, length, Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /** + * Serializes an object and returns the Base64-encoded version of that serialized object.

As of v 2.3, if + * the object cannot be serialized or there is another error, the method will throw an java.io.IOException. This + * is new to v2.3! In earlier versions, it just returned a null value, but in retrospect that's a pretty poor + * way to next it.

+ *

+ * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * + * @return The Base64-encoded object + * + * @throws java.io.IOException if there is an error + * @throws NullPointerException if serializedObject is null + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) throws java.io.IOException { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + /** + * Serializes an object and returns the Base64-encoded version of that serialized object.

As of v 2.3, if + * the object cannot be serialized or there is another error, the method will throw an java.io.IOException. This + * is new to v2.3! In earlier versions, it just returned a null value, but in retrospect that's a pretty poor + * way to next it.

+ *

+ * The object is not GZip-compressed before being encoded. + *

+ * Example options: + *

+ *

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     * 
+ *

+ * Example: encodeObject( myObj, Base64.GZIP ) or + *

+ * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * @param serializableObject The object to encode + * @param options Specified options + * + * @return The Base64-encoded object + * + * @throws java.io.IOException if there is an error + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, int options) throws java.io.IOException { + + if (serializableObject == null) + throw new NullPointerException("Cannot serialize a null object."); + + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.io.ObjectOutputStream oos = null; + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + // Note that the optional GZIPping is handled by Base64.OutputStream. + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + oos = new java.io.ObjectOutputStream(b64os); + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + // Fall back to some Java default + return new String(baos.toByteArray()); + } // end catch + + } // end encode + + /** + * Convenience method for encoding data to a file.

As of v 2.3, if there is a error, the method will throw + * an java.io.IOException. This is new to v2.3! In earlier versions, it just returned false, but in + * retrospect that's a pretty poor way to next it.

+ * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * + * @throws java.io.IOException if there is an error + * @throws NullPointerException if dataToEncode is null + * @since 2.1 + */ + public static void encodeToFile(byte[] dataToEncode, String filename) throws java.io.IOException { + + if (dataToEncode == null) + throw new NullPointerException("Data to encode was null."); + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream(new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + } // end encodeToFile + + /** + * Decodes four bytes from array source and writes the resulting bytes (up to three of them) to + * destination. The source and destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 4 for the source array or destOffset + * + 3 for the destination array. This method returns the actual number of bytes that were converted from + * the Base64 encoding.

This is the lowest level of the decoding method with all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * + * @return the number of decoded bytes converted + * + * @throws NullPointerException if source or destination arrays are null + * @throws IllegalArgumentException if srcOffset or destOffset are invalid or there is not enough room in the + * array. + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset, int options) { + + // Lots of error checking and exception throwing + if (source == null) + throw new NullPointerException("Source array was null."); + if (destination == null) + throw new NullPointerException("Destination array was null."); + if (srcOffset < 0 || srcOffset + 3 >= source.length) + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", + source.length, srcOffset)); + if (destOffset < 0 || destOffset + 2 >= destination.length) + throw new IllegalArgumentException(String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", + destination.length, destOffset)); + + byte[] DECODABET = getDecodabet(options); + + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = (DECODABET[source[srcOffset]] & 0xFF) << 18 | (DECODABET[source[srcOffset + 1]] & 0xFF) << 12; + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = (DECODABET[source[srcOffset]] & 0xFF) << 18 | (DECODABET[source[srcOffset + 1]] & 0xFF) << 12 + | (DECODABET[source[srcOffset + 2]] & 0xFF) << 6; + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = (DECODABET[source[srcOffset]] & 0xFF) << 18 | (DECODABET[source[srcOffset + 1]] & 0xFF) << 12 + | (DECODABET[source[srcOffset + 2]] & 0xFF) << 6 | DECODABET[source[srcOffset + 3]] & 0xFF; + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) outBuff; + + return 3; + } + } // end decodeToBytes + + /** + * Encodes up to the first three bytes of array threeBytes and returns a four-byte array in Base64 + * notation. The actual number of significant bytes in your array is given by numSigBytes. The array + * threeBytes needs only be as big as numSigBytes. Code can reuse a byte array by passing a + * four-byte array as b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * + * @return four byte array in Base64 notation. + * + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); + return b4; + } // end encode3to4 + + /** + *

Encodes up to three bytes of the array source and writes the resulting four Base64 bytes to + * destination. The source and destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 3 for the source array or destOffset + * + 4 for the destination array. The actual number of significant bytes in your array is given by + * numSigBytes.

This is the lowest level of the encoding method with all possible parameters. + *

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * + * @return the destination array + * + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination, int destOffset, + int options) { + + byte[] ALPHABET = getAlphabet(options); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = (numSigBytes > 0 ? source[srcOffset] << 24 >>> 8 : 0) + | (numSigBytes > 1 ? source[srcOffset + 1] << 24 >>> 16 : 0) + | (numSigBytes > 2 ? source[srcOffset + 2] << 24 >>> 24 : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[inBuff >>> 12 & 0x3f]; + destination[destOffset + 2] = ALPHABET[inBuff >>> 6 & 0x3f]; + destination[destOffset + 3] = ALPHABET[inBuff & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[inBuff >>> 12 & 0x3f]; + destination[destOffset + 2] = ALPHABET[inBuff >>> 6 & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[inBuff >>> 12 & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options specified. It's possible, though + * silly, to specify ORDERED and URLSAFE in which case one of them will be picked, though there is no + * guarantee as to which one will be picked. + */ + private static byte[] getAlphabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) + return _URL_SAFE_ALPHABET; + else if ((options & ORDERED) == ORDERED) + return _ORDERED_ALPHABET; + else + return _STANDARD_ALPHABET; + } // end getAlphabet + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on the options specified. It's possible, though + * silly, to specify ORDERED and URL_SAFE in which case one of them will be picked, though there is no guarantee as + * to which one will be picked. + */ + private static byte[] getDecodabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) + return _URL_SAFE_DECODABET; + else if ((options & ORDERED) == ORDERED) + return _ORDERED_DECODABET; + else + return _STANDARD_DECODABET; + } // end getAlphabet + + /** Defeats instantiation. */ + private Base64() { + } + +} // end class Base64 \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/common/Buffer.java b/src/main/java/net/schmizz/sshj/common/Buffer.java new file mode 100644 index 00000000..3dfc3970 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/Buffer.java @@ -0,0 +1,531 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Arrays; + +public class Buffer> { + + public static class BufferException extends SSHRuntimeException { + public BufferException(String message) { + super(message); + } + } + + /** The default size for a {@code Buffer} (256 bytes) */ + public static final int DEFAULT_SIZE = 256; + + protected static int getNextPowerOf2(int i) { + int j = 1; + while (j < i) + j <<= 1; + return j; + } + + protected byte[] data; + protected int rpos; + protected int wpos; + + /** @see {@link #DEFAULT_SIZE} */ + public Buffer() { + this(DEFAULT_SIZE); + } + + public Buffer(Buffer from) { + data = new byte[(wpos = from.wpos - from.rpos)]; + System.arraycopy(from.data, from.rpos, data, 0, wpos); + } + + public Buffer(byte[] data) { + this(data, true); + } + + public Buffer(int size) { + this(new byte[getNextPowerOf2(size)], false); + } + + private Buffer(byte[] data, boolean read) { + this.data = data; + rpos = 0; + wpos = read ? data.length : 0; + } + + public byte[] array() { + return data; + } + + public int available() { + return wpos - rpos; + } + + /** Resets this buffer. The object becomes ready for reuse. */ + public void clear() { + rpos = 0; + wpos = 0; + } + + public int rpos() { + return rpos; + } + + public void rpos(int rpos) { + this.rpos = rpos; + } + + public int wpos() { + return wpos; + } + + public void wpos(int wpos) { + ensureCapacity(wpos - this.wpos); + this.wpos = wpos; + } + + protected void ensureAvailable(int a) { + if (available() < a) + throw new BufferException("Underflow"); + } + + public void ensureCapacity(int capacity) { + if (data.length - wpos < capacity) { + int cw = wpos + capacity; + byte[] tmp = new byte[getNextPowerOf2(cw)]; + System.arraycopy(data, 0, tmp, 0, data.length); + data = tmp; + } + } + + /** Compact this {@link SSHPacket} */ + public void compact() { + System.err.println("COMPACTING"); + if (available() > 0) + System.arraycopy(data, rpos, data, 0, wpos - rpos); + wpos -= rpos; + rpos = 0; + } + + public byte[] getCompactData() { + final int len = available(); + if (len > 0) { + byte[] b = new byte[len]; + System.arraycopy(data, rpos, b, 0, len); + return b; + } else + return new byte[0]; + } + + /** + * Read an SSH boolean byte + * + * @return the {@code true} or {@code false} value read + */ + public boolean readBoolean() { + return readByte() != 0; + } + + /** + * Puts an SSH boolean value + * + * @param b the value + * + * @return this + */ + public T putBoolean(boolean b) { + return putByte(b ? (byte) 1 : (byte) 0); + } + + /** + * Read a byte from the buffer + * + * @return the byte read + */ + public byte readByte() { + ensureAvailable(1); + return data[rpos++]; + } + + /** + * Writes a single byte into this buffer + * + * @param b + * + * @return this + */ + @SuppressWarnings("unchecked") + public T putByte(byte b) { + ensureCapacity(1); + data[wpos++] = b; + return (T) this; + } + + /** + * Read an SSH byte-array + * + * @return the byte-array read + */ + public byte[] readBytes() { + int len = readInt(); + if (len < 0 || len > 32768) + throw new BufferException("Bad item length: " + len); + byte[] b = new byte[len]; + readRawBytes(b); + return b; + } + + /** + * Writes Java byte-array as an SSH byte-array + * + * @param b Java byte-array + * + * @return this + */ + public T putBytes(byte[] b) { + return putBytes(b, 0, b.length); + } + + /** + * Writes Java byte-array as an SSH byte-array + * + * @param b Java byte-array + * @param off offset + * @param len length + * + * @return this + */ + public T putBytes(byte[] b, int off, int len) { + return putInt(len - off).putRawBytes(b, off, len); + } + + public void readRawBytes(byte[] buf) { + readRawBytes(buf, 0, buf.length); + } + + public void readRawBytes(byte[] buf, int off, int len) { + ensureAvailable(len); + System.arraycopy(data, rpos, buf, off, len); + rpos += len; + } + + public T putRawBytes(byte[] d) { + return putRawBytes(d, 0, d.length); + } + + @SuppressWarnings("unchecked") + public T putRawBytes(byte[] d, int off, int len) { + ensureCapacity(len); + System.arraycopy(d, off, data, wpos, len); + wpos += len; + return (T) this; + } + + /** + * Copies the contents of provided buffer into this buffer + * + * @param buffer the {@code Buffer} to copy + * + * @return this + */ + @SuppressWarnings("unchecked") + public T putBuffer(Buffer> buffer) { + if (buffer != null) { + int r = buffer.available(); + ensureCapacity(r); + System.arraycopy(buffer.data, buffer.rpos, data, wpos, r); + wpos += r; + } + return (T) this; + } + + public int readInt() { + return (int) readLong(); + } + + public long readLong() { + ensureAvailable(4); + return data[rpos++] << 24 & 0xff000000L | + data[rpos++] << 16 & 0x00ff0000L | + data[rpos++] << 8 & 0x0000ff00L | + data[rpos++] & 0x000000ffL; + } + + /** + * Writes a uint32 integer + * + * @param uint32 + * + * @return this + */ + @SuppressWarnings("unchecked") + public T putInt(long uint32) { + ensureCapacity(4); + if (uint32 < 0 || uint32 > 0xffffffffL) + throw new BufferException("Invalid value: " + uint32); + data[wpos++] = (byte) (uint32 >> 24); + data[wpos++] = (byte) (uint32 >> 16); + data[wpos++] = (byte) (uint32 >> 8); + data[wpos++] = (byte) uint32; + return (T) this; + } + + /** + * Read an SSH multiple-precision integer + * + * @return the MP integer as a {@code BigInteger} + */ + public BigInteger readMPInt() { + return new BigInteger(readMPIntAsBytes()); + } + + /** + * Writes an SSH multiple-precision integer from a {@code BigInteger} + * + * @param bi {@code BigInteger} to write + * + * @return this + */ + public T putMPInt(BigInteger bi) { + return putMPInt(bi.toByteArray()); + } + + /** + * Writes an SSH multiple-precision integer from a Java byte-array + * + * @param foo byte-array + * + * @return this + */ + public T putMPInt(byte[] foo) { + int i = foo.length; + if ((foo[0] & 0x80) != 0) { + i++; + putInt(i); + putByte((byte) 0); + } else + putInt(i); + return putRawBytes(foo); + } + + public byte[] readMPIntAsBytes() { + return readBytes(); + } + + public long readUINT64() { + long uint64 = (readLong() << 32) + (readLong() & 0xffffffffL); + if (uint64 < 0) + throw new BufferException("Cannot handle values > Long.MAX_VALUE"); + return uint64; + } + + @SuppressWarnings("unchecked") + public T putUINT64(long uint64) { + if (uint64 < 0) + throw new BufferException("Invalid value: " + uint64); + data[wpos++] = (byte) (uint64 >> 56); + data[wpos++] = (byte) (uint64 >> 48); + data[wpos++] = (byte) (uint64 >> 40); + data[wpos++] = (byte) (uint64 >> 32); + data[wpos++] = (byte) (uint64 >> 24); + data[wpos++] = (byte) (uint64 >> 16); + data[wpos++] = (byte) (uint64 >> 8); + data[wpos++] = (byte) uint64; + return (T) this; + } + + /** + * Reads an SSH string + * + * @return the string as a Java {@code String} + */ + public String readString() { + int len = readInt(); + if (len < 0 || len > 32768) + throw new BufferException("Bad item length: " + len); + ensureAvailable(len); + String s; + try { + s = new String(data, rpos, len, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new SSHRuntimeException(e); + } + rpos += len; + return s; + } + + /** + * Reads an SSH string + * + * @return the string as a byte-array + */ + public byte[] readStringAsBytes() { + return readBytes(); + } + + public T putString(byte[] str) { + return putBytes(str); + } + + public T putString(byte[] str, int offset, int len) { + return putBytes(str, offset, len); + } + + public T putString(String string) { + try { + return putString(string.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new SSHRuntimeException(e); + } + } + + /** + * Writes a char-array as an SSH string and then blanks it out. + *

+ * This is useful when a plaintext password needs to be sent. If {@code passwd} is {@code null}, an empty string is + * written. + * + * @param passwd (null-ok) the password as a character array + * + * @return this + */ + @SuppressWarnings("unchecked") + public T putPassword(char[] passwd) { + if (passwd == null) + return putString(""); + putInt(passwd.length); + ensureCapacity(passwd.length); + for (char c : passwd) + data[wpos++] = (byte) c; + Arrays.fill(passwd, ' '); + return (T) this; + } + + public PublicKey readPublicKey() { + PublicKey key = null; + try { + switch (KeyType.fromString(readString())) { + case RSA: { + BigInteger e = readMPInt(); + BigInteger n = readMPInt(); + KeyFactory keyFactory = SecurityUtils.getKeyFactory("RSA"); + key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); + break; + } + case DSA: { + BigInteger p = readMPInt(); + BigInteger q = readMPInt(); + BigInteger g = readMPInt(); + BigInteger y = readMPInt(); + KeyFactory keyFactory = SecurityUtils.getKeyFactory("DSA"); + key = keyFactory.generatePublic(new DSAPublicKeySpec(y, p, q, g)); + break; + } + default: + assert false; + } + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + return key; + } + + @SuppressWarnings("unchecked") + public T putPublicKey(PublicKey key) { + KeyType type = KeyType.fromKey(key); + switch (type) { + case RSA: + putString(type.toString()) // ssh-rsa + .putMPInt(((RSAPublicKey) key).getPublicExponent()) // e + .putMPInt(((RSAPublicKey) key).getModulus()); // n + break; + case DSA: + putString(type.toString()) // ssh-dss + .putMPInt(((DSAPublicKey) key).getParams().getP()) // p + .putMPInt(((DSAPublicKey) key).getParams().getQ()) // q + .putMPInt(((DSAPublicKey) key).getParams().getG()) // g + .putMPInt(((DSAPublicKey) key).getY()); // y + break; + default: + assert false; + } + return (T) this; + } + + public T putSignature(String sigFormat, byte[] sigData) { + return putString(new PlainBuffer().putString(sigFormat).putBytes(sigData).getCompactData()); + } + + /** + * Gives a readable snapshot of the buffer in hex. This is useful for debugging. + * + * @return snapshot of the buffer as a hex string with each octet delimited by a space + */ + public String printHex() { + return ByteArrayUtils.printHex(array(), rpos(), available()); + } + + @Override + public String toString() { + return "Buffer [rpos=" + rpos + ", wpos=" + wpos + ", size=" + data.length + "]"; + } + + public static class PlainBuffer extends Buffer { + + public PlainBuffer() { + super(); + } + + public PlainBuffer(Buffer from) { + super(from); + } + + public PlainBuffer(byte[] b) { + super(b); + } + + public PlainBuffer(int size) { + super(size); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java new file mode 100644 index 00000000..caac6886 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java @@ -0,0 +1,146 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import java.util.Arrays; + +/** Utility functions for byte arrays. */ +public class ByteArrayUtils { + + final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Check whether two byte arrays are the equal. + * + * @param a1 + * @param a2 + * + * @return true or false + */ + public static boolean equals(byte[] a1, byte[] a2) { + return (a1.length != a2.length && equals(a1, 0, a2, 0, a1.length)); + } + + /** + * Check whether some part or whole of two byte arrays is equal, for length bytes starting at some + * offset. + * + * @param a1 + * @param a1Offset + * @param a2 + * @param a2Offset + * @param length + * + * @return true or false + */ + public static boolean equals(byte[] a1, int a1Offset, byte[] a2, int a2Offset, int length) { + if (a1.length < a1Offset + length || a2.length < a2Offset + length) + return false; + while (length-- > 0) + if (a1[a1Offset++] != a2[a2Offset++]) + return false; + return true; + } + + /** + * Get a hexadecimal representation of array, with each octet separated by a space. + * + * @param array + * + * @return hex string, each octet delimited by a space + */ + public static String printHex(byte[] array) { + return printHex(array, 0, array.length); + } + + /** + * Get a hexadecimal representation of a byte array starting at offset index for len + * bytes, with each octet separated by a space. + * + * @param array + * @param offset + * @param len + * + * @return hex string, each octet delimited by a space + */ + public static String printHex(byte[] array, int offset, int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + byte b = array[offset + i]; + if (sb.length() > 0) + sb.append(' '); + sb.append(digits[b >> 4 & 0x0F]); + sb.append(digits[b & 0x0F]); + } + return sb.toString(); + } + + /** + * Get the hexadecimal representation of a byte array. + * + * @param array + * + * @return hex string + */ + public static String toHex(byte[] array) { + return toHex(array, 0, array.length); + } + + /** + * Get the hexadecimal representation of a byte array starting at offset index for len + * bytes. + * + * @param array + * @param offset + * @param len + * + * @return hex string + */ + public static String toHex(byte[] array, int offset, int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + byte b = array[offset + i]; + sb.append(digits[b >> 4 & 0x0F]); + sb.append(digits[b & 0x0F]); + } + return sb.toString(); + } + + public static byte[] copyOf(byte[] array) { + return Arrays.copyOf(array, array.length); + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/DisconnectReason.java b/src/main/java/net/schmizz/sshj/common/DisconnectReason.java new file mode 100644 index 00000000..5712f4fe --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/DisconnectReason.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.common; + +/** Disconnect error codes */ +public enum DisconnectReason { + + UNKNOWN(0), + HOST_NOT_ALLOWED_TO_CONNECT(1), + PROTOCOL_ERROR(2), + KEY_EXCHANGE_FAILED(3), + HOST_AUTHENTICATION_FAILED(4), + RESERVED(4), + MAC_ERROR(5), + COMPRESSION_ERROR(6), + SERVICE_NOT_AVAILABLE(7), + PROTOCOL_VERSION_NOT_SUPPORTED(8), + HOST_KEY_NOT_VERIFIABLE(9), + CONNECTION_LOST(10), + BY_APPLICATION(11), + TOO_MANY_CONNECTIONS(12), + AUTH_CANCELLED_BY_USER(13), + NO_MORE_AUTH_METHODS_AVAILABLE(14), + ILLEGAL_USER_NAME(15); + + public static DisconnectReason fromInt(int code) { + for (DisconnectReason dc : values()) + if (dc.code == code) + return dc; + return UNKNOWN; + } + + private final int code; + + private DisconnectReason(int code) { + this.code = code; + } + + public int toInt() { + return code; + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/ErrorNotifiable.java b/src/main/java/net/schmizz/sshj/common/ErrorNotifiable.java new file mode 100644 index 00000000..b30ecf18 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/ErrorNotifiable.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.common; + +import java.util.Collection; + +/** API for classes that are capable of being notified on an error so they can cleanup. */ +public interface ErrorNotifiable { + + /** Utility functions. */ + class Util { + /** Notify all {@code notifiables} of given {@code error}. */ + public static void alertAll(SSHException error, ErrorNotifiable... notifiables) { + for (ErrorNotifiable notifiable : notifiables) + notifiable.notifyError(error); + } + + /** Notify all {@code notifiables} of given {@code error}. */ + public static void alertAll(SSHException error, Collection notifiables) { + for (ErrorNotifiable notifiable : notifiables) + notifiable.notifyError(error); + } + } + + /** Notifies this object of an {@code error}. */ + void notifyError(SSHException error); + +} diff --git a/src/main/java/net/schmizz/sshj/common/Factory.java b/src/main/java/net/schmizz/sshj/common/Factory.java new file mode 100644 index 00000000..566b6449 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/Factory.java @@ -0,0 +1,119 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import java.util.LinkedList; +import java.util.List; + +/** + * A basic factory interface. + * + * @param the type of object created by this factory + */ +public interface Factory { + + /** + * Inteface for a named factory. Named factories are simply factories that are identified by a name. Such names are + * used mainly in SSH algorithm negotiation. + * + * @param type of object created by this factory + */ + interface Named extends Factory { + + /** Utility functions */ + public static class Util { + + /** + * Creates an object by picking a factory from {@code factories} that is identified by {@code name} from a + * list of named {@code factories}. Uses the first match. + * + * @param factories list of available factories + * @param name name of the desired factory + * @param type of the {@code factories} + * + * @return a newly created instance of {@code T} or {@code null} if there was no match + */ + public static T create(List> factories, String name) { + if (factories != null) + for (Named f : factories) + if (f.getName().equals(name)) + return f.create(); + return null; + } + + /** + * Retrieve a particular factory as identified by {@code name} from a list of named {@code factories}. + * Returns the first match. + * + * @param factories list of factories + * @param name the name of the factory to retrieve + * @param type of the {@code factories} + * + * @return a factory or {@code null} if there was no match + */ + public static Named get(List> factories, String name) { + for (Named f : factories) + if (f.getName().equals(name)) + return f; + return null; + } + + /** + * Get a comma-delimited string containing the factory names from the given list of {@code factories}. + * + * @param factories list of available factories + * @param type of the {@code factories} + * + * @return a comma separated list of factory names + */ + public static List getNames(List> factories) { + List list = new LinkedList(); + for (Named f : factories) + list.add(f.getName()); + return list; + } + + } + + /** @return the name of this factory. */ + String getName(); + + } + + /** @return a new object created using this factory. */ + T create(); + +} diff --git a/src/main/java/net/schmizz/sshj/common/IOUtils.java b/src/main/java/net/schmizz/sshj/common/IOUtils.java new file mode 100644 index 00000000..e4d0f372 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/IOUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; + +public class IOUtils { + + private static final Logger LOG = LoggerFactory.getLogger(IOUtils.class); + + public static void closeQuietly(Closeable... closeables) { + for (Closeable c : closeables) + try { + if (c != null) + c.close(); + } catch (IOException logged) { + LOG.warn("Error closing {} - {}", c, logged); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/KeyType.java b/src/main/java/net/schmizz/sshj/common/KeyType.java new file mode 100644 index 00000000..08c1b355 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/KeyType.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.common; + +import java.security.Key; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +public enum KeyType { + + /** SSH identifier for RSA keys */ + RSA("ssh-rsa", new KeyChecker() { + public boolean isMyType(Key key) { + return (key instanceof RSAPublicKey || key instanceof RSAPrivateKey); + } + }), + + /** SSH identifier for DSA keys */ + DSA("ssh-dss", new KeyChecker() { + public boolean isMyType(Key key) { + return (key instanceof DSAPublicKey || key instanceof DSAPrivateKey); + } + }), + + /** Unrecognized */ + UNKNOWN("unknown", null); + + private static interface KeyChecker { + boolean isMyType(Key key); + } + + private final String sType; + private final KeyChecker checker; + + private KeyType(String type, KeyChecker checker) { + this.sType = type; + this.checker = checker; + } + + public static KeyType fromKey(Key key) { + for (KeyType kt : values()) + if (kt.checker != null && kt.checker.isMyType((key))) + return kt; + return UNKNOWN; + } + + public static KeyType fromString(String sType) { + for (KeyType kt : values()) + if (kt.sType.equals(sType)) + return kt; + return UNKNOWN; + } + + @Override + public String toString() { + return sType; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/common/Message.java b/src/main/java/net/schmizz/sshj/common/Message.java new file mode 100644 index 00000000..e9b59fe1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/Message.java @@ -0,0 +1,106 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.common; + +/** SSH message identifiers */ +public enum Message { + + DISCONNECT(1), + IGNORE(2), + UNIMPLEMENTED(3), + DEBUG(4), + SERVICE_REQUEST(5), + SERVICE_ACCEPT(6), + KEXINIT(20), + NEWKEYS(21), + + KEXDH_INIT(30), + + /** { KEXDH_REPLY, KEXDH_GEX_GROUP } */ + KEXDH_31(31), + + KEX_DH_GEX_INIT(32), + KEX_DH_GEX_REPLY(33), + KEX_DH_GEX_REQUEST(34), + + USERAUTH_REQUEST(50), + USERAUTH_FAILURE(51), + USERAUTH_SUCCESS(52), + USERAUTH_BANNER(53), + + /** { USERAUTH_PASSWD_CHANGREQ, USERAUTH_PK_OK, USERAUTH_INFO_REQUEST } */ + USERAUTH_60(60), + USERAUTH_INFO_RESPONSE(61), + + GLOBAL_REQUEST(80), + REQUEST_SUCCESS(81), + REQUEST_FAILURE(82), + + CHANNEL_OPEN(90), + CHANNEL_OPEN_CONFIRMATION(91), + CHANNEL_OPEN_FAILURE(92), + CHANNEL_WINDOW_ADJUST(93), + CHANNEL_DATA(94), + CHANNEL_EXTENDED_DATA(95), + CHANNEL_EOF(96), + CHANNEL_CLOSE(97), + CHANNEL_REQUEST(98), + CHANNEL_SUCCESS(99), + CHANNEL_FAILURE(100); + + private final byte b; + + private static final Message[] commands = new Message[256]; + + static { + for (Message c : Message.values()) + if (commands[c.toByte()] == null) + commands[c.toByte()] = c; + } + + public static Message fromByte(byte b) { + return commands[b]; + } + + Message(int b) { + this.b = (byte) b; + } + + public boolean geq(int num) { + return b >= num; + } + + public boolean gt(int num) { + return b > num; + } + + public boolean in(int x, int y) { + return b >= x && b <= y; + } + + public boolean leq(int num) { + return b <= num; + } + + public boolean lt(int num) { + return b < num; + } + + public byte toByte() { + return b; + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/SSHException.java b/src/main/java/net/schmizz/sshj/common/SSHException.java new file mode 100644 index 00000000..87fc18bf --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/SSHException.java @@ -0,0 +1,122 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import net.schmizz.concurrent.ExceptionChainer; + +import java.io.IOException; + +/** + * Most exceptions in {@code org.apache.commons.net.ssh} are instances of this class. An {@link SSHException} is itself + * an {@link IOException} and can be caught like that if this level of granularity is not desired. + */ +public class SSHException extends IOException { + + public static final ExceptionChainer chainer = new ExceptionChainer() { + + public SSHException chain(Throwable t) { + if (t instanceof SSHException) + return (SSHException) t; + else + return new SSHException(t); + } + + }; + + private final DisconnectReason reason; + + public SSHException() { + this(DisconnectReason.UNKNOWN, null, null); + } + + public SSHException(DisconnectReason code) { + this(code, null, null); + } + + public SSHException(DisconnectReason code, String message) { + this(code, message, null); + } + + public SSHException(DisconnectReason code, String message, Throwable cause) { + super(message); + this.reason = code; + if (cause != null) + initCause(cause); + } + + public SSHException(DisconnectReason code, Throwable cause) { + this(code, null, cause); + } + + public SSHException(String message) { + this(DisconnectReason.UNKNOWN, message, null); + } + + public SSHException(String message, Throwable cause) { + this(DisconnectReason.UNKNOWN, message, cause); + } + + public SSHException(Throwable cause) { + this(DisconnectReason.UNKNOWN, null, cause); + } + + public int getDisconnectCode() { + return reason.toInt(); + } + + public DisconnectReason getDisconnectReason() { + return reason; + } + + @Override + public String getMessage() { + if (super.getMessage() != null) + return super.getMessage(); + else if (getCause() != null && getCause().getMessage() != null) + return getCause().getMessage(); + else + return null; + } + + @Override + public String toString() { + final String cls = getClass().getName(); + final String code = reason != DisconnectReason.UNKNOWN ? "[" + reason + "] " : ""; + final String msg = getMessage() != null ? getMessage() : ""; + return cls + (code.equals("") && msg.equals("") ? "" : ": ") + code + msg; + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/SSHPacket.java b/src/main/java/net/schmizz/sshj/common/SSHPacket.java new file mode 100644 index 00000000..4cc0fb9d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/SSHPacket.java @@ -0,0 +1,96 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import java.util.Arrays; + +public class SSHPacket extends Buffer { + + public SSHPacket() { + super(); + } + + public SSHPacket(int size) { + super(size); + } + + public SSHPacket(byte[] data) { + super(data); + } + + /** + * Constructs new buffer for the specified SSH packet and reserves the needed space (5 bytes) for the packet + * header. + * + * @param msg the SSH command + */ + public SSHPacket(Message msg) { + super(); + rpos = wpos = 5; + putMessageID(msg); + } + + public SSHPacket(SSHPacket p) { + this.data = Arrays.copyOf(p.data, p.wpos); + this.rpos = p.rpos; + this.wpos = p.wpos; + } + + /** + * Reads an SSH byte and returns it as {@link Message} + * + * @return the message identifier + */ + public Message readMessageID() { + byte b = readByte(); + Message cmd = Message.fromByte(b); + if (cmd == null) + throw new BufferException("Unknown message ID: " + b); + return cmd; + } + + /** + * Writes a byte indicating the SSH message identifier + * + * @param msg the identifier as a {@link Message} type + * + * @return this + */ + public SSHPacket putMessageID(Message msg) { + return putByte(msg.toByte()); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/common/SSHPacketHandler.java b/src/main/java/net/schmizz/sshj/common/SSHPacketHandler.java new file mode 100644 index 00000000..e8913703 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/SSHPacketHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.common; + +/** + * An interface for classes to which packet handling may be delegated. Chains of such delegations may be used, e.g. + * {@code packet decoder -> (SSHPacketHandler) transport layer -> (SSHPacketHandler) connection layer -> + * (SSHPacketHandler) channel}. + */ +public interface SSHPacketHandler { + + /** + * Delegate handling of some SSH packet to this object. + * + * @param msg the SSH {@link Message message identifier} + * @param buf {@link SSHPacket} containing rest of the request + * + * @throws SSHException if there is a non-recoverable error + */ + void handle(Message msg, SSHPacket buf) throws SSHException; + +} diff --git a/src/main/java/net/schmizz/sshj/common/SSHRuntimeException.java b/src/main/java/net/schmizz/sshj/common/SSHRuntimeException.java new file mode 100644 index 00000000..92843343 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/SSHRuntimeException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +/** Represents unrecoverable exceptions in the {@code org.apache.commons.net.ssh} package. */ +public class SSHRuntimeException extends RuntimeException { + + public SSHRuntimeException() { + this(null, null); + } + + public SSHRuntimeException(String message) { + this(message, null); + } + + public SSHRuntimeException(String message, Throwable cause) { + super(message); + if (cause != null) + initCause(cause); + } + + public SSHRuntimeException(Throwable cause) { + this(null, cause); + } + +} diff --git a/src/main/java/net/schmizz/sshj/common/SecurityUtils.java b/src/main/java/net/schmizz/sshj/common/SecurityUtils.java new file mode 100644 index 00000000..0b37374c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/SecurityUtils.java @@ -0,0 +1,277 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.common; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.Signature; + +/** Static utility method relating to security facilities. */ +public class SecurityUtils { + + private static class BouncyCastleRegistration { + public void run() throws Exception { + if (java.security.Security.getProvider(BOUNCY_CASTLE) == null) { + LOG.info("Trying to register BouncyCastle as a JCE provider"); + java.security.Security.addProvider(new BouncyCastleProvider()); + MessageDigest.getInstance("MD5", BOUNCY_CASTLE); + KeyAgreement.getInstance("DH", BOUNCY_CASTLE); + LOG.info("Registration succeeded"); + } else + LOG.info("BouncyCastle already registered as a JCE provider"); + securityProvider = BOUNCY_CASTLE; + } + } + + private static final Logger LOG = LoggerFactory.getLogger(SecurityUtils.class); + + /** Identifier for the BouncyCastle JCE provider */ + public static final String BOUNCY_CASTLE = "BC"; + + /* + * Security provider identifier. null = default JCE + */ + private static String securityProvider = null; + + // relate to BC registration + private static Boolean registerBouncyCastle; + private static boolean registrationDone; + + public static synchronized Cipher getCipher(String transformation) throws NoSuchAlgorithmException, + NoSuchPaddingException, NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return Cipher.getInstance(transformation); + else + return Cipher.getInstance(transformation, getSecurityProvider()); + } + + /** + * Computes the fingerprint for a public key, in the standard SSH format, e.g. "4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21" + * + * @param key the public key + * + * @return the fingerprint + * + * @see specification + */ + public static String getFingerprint(PublicKey key) { + MessageDigest md5; + try { + md5 = getMessageDigest("MD5"); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + md5.update(new Buffer.PlainBuffer().putPublicKey(key).getCompactData()); + final String undelimited = ByteArrayUtils.toHex(md5.digest()); + assert undelimited.length() == 32 : "md5 contract"; + StringBuilder fp = new StringBuilder(undelimited.substring(0, 2)); + for (int i = 2; i <= undelimited.length() - 2; i += 2) + fp.append(":").append(undelimited.substring(i, i + 2)); + return fp.toString(); + } + + /** + * Creates a new instance of {@link KeyAgreement} with the given algorithm. + * + * @param algorithm key agreement algorithm + * + * @return new instance + * + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + */ + public static synchronized KeyAgreement getKeyAgreement(String algorithm) throws NoSuchAlgorithmException, + NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return KeyAgreement.getInstance(algorithm); + else + return KeyAgreement.getInstance(algorithm, getSecurityProvider()); + } + + /** + * Creates a new instance of {@link KeyFactory} with the given algorithm. + * + * @param algorithm key factory algorithm e.g. RSA, DSA + * + * @return new instance + * + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + */ + public static synchronized KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException, + NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return KeyFactory.getInstance(algorithm); + else + return KeyFactory.getInstance(algorithm, getSecurityProvider()); + } + + /** + * Creates a new instance of {@link KeyPairGenerator} with the given algorithm. + * + * @param algorithm key pair generator algorithm + * + * @return new instance + * + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + */ + public static synchronized KeyPairGenerator getKeyPairGenerator(String algorithm) throws NoSuchAlgorithmException, + NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return KeyPairGenerator.getInstance(algorithm); + else + return KeyPairGenerator.getInstance(algorithm, getSecurityProvider()); + } + + /** + * Create a new instance of {@link Mac} with the given algorithm. + * + * @param algorithm MAC algorithm + * + * @return new instance + * + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + */ + public static synchronized Mac getMAC(String algorithm) throws NoSuchAlgorithmException, NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return Mac.getInstance(algorithm); + else + return Mac.getInstance(algorithm, getSecurityProvider()); + } + + /** + * Create a new instance of {@link MessageDigest} with the given algorithm. + * + * @param algorithm MessageDigest algorithm name + * + * @return + * + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + */ + public static synchronized MessageDigest getMessageDigest(String algorithm) throws NoSuchAlgorithmException, + NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return MessageDigest.getInstance(algorithm); + else + return MessageDigest.getInstance(algorithm, getSecurityProvider()); + } + + /** + * Get the identifier for the registered security provider. + * + * @return JCE provider identifier + */ + public static synchronized String getSecurityProvider() { + register(); + return securityProvider; + } + + public static synchronized Signature getSignature(String algorithm) throws NoSuchAlgorithmException, + NoSuchProviderException { + register(); + if (getSecurityProvider() == null) + return Signature.getInstance(algorithm); + else + return Signature.getInstance(algorithm, getSecurityProvider()); + } + + /** + * Attempts registering BouncyCastle as security provider if it has not been previously attempted and returns + * whether the registration succeeded. + * + * @return whether BC registered + */ + public static synchronized boolean isBouncyCastleRegistered() { + register(); + return BOUNCY_CASTLE.equals(securityProvider); + } + + public static synchronized void setRegisterBouncyCastle(boolean registerBouncyCastle) { + SecurityUtils.registerBouncyCastle = registerBouncyCastle; + registrationDone = false; + } + + /** + * Specifies the JCE security provider that should be used. + * + * @param securityProvider identifier for the security provider + */ + public static synchronized void setSecurityProvider(String securityProvider) { + SecurityUtils.securityProvider = securityProvider; + registrationDone = false; + } + + private static void register() { + if (!registrationDone) { + if (securityProvider == null && (registerBouncyCastle == null || registerBouncyCastle)) + // Use an inner class to avoid a strong dependency on BouncyCastle + try { + new BouncyCastleRegistration().run(); + } catch (Throwable t) { + if (registerBouncyCastle == null) + LOG.info("BouncyCastle not registered, using the default JCE provider"); + else { + LOG.error("Failed to register BouncyCastle as the defaut JCE provider"); + throw new SSHRuntimeException("Failed to register BouncyCastle as the defaut JCE provider", t); + } + } + registrationDone = true; + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/common/StreamCopier.java b/src/main/java/net/schmizz/sshj/common/StreamCopier.java new file mode 100644 index 00000000..d4da021e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/common/StreamCopier.java @@ -0,0 +1,128 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +public class StreamCopier extends Thread { + + private static final Logger LOG = LoggerFactory.getLogger(StreamCopier.class); + + public interface ErrorCallback { + void onError(IOException ioe); + } + + public static ErrorCallback closeOnErrorCallback(final Closeable... toClose) { + final Closeable[] closeables = Arrays.copyOf(toClose, toClose.length); + return new ErrorCallback() { + public void onError(IOException ioe) { + IOUtils.closeQuietly(closeables); + } + }; + } + + public static long copy(InputStream in, OutputStream out, int bufSize, boolean keepFlushing) throws IOException { + long count = 0; + + final long startTime = System.currentTimeMillis(); + + final byte[] buf = new byte[bufSize]; + int read; + while ((read = in.read(buf)) != -1) { + out.write(buf, 0, read); + count += read; + if (keepFlushing) + out.flush(); + } + if (!keepFlushing) + out.flush(); + + final double sizeKiB = count / 1024.0; + final double timeSeconds = (System.currentTimeMillis() - startTime) / 1000.0; + LOG.info(sizeKiB + " KiB transferred in {} seconds ({} KiB/s)", timeSeconds, (sizeKiB / timeSeconds)); + + return count; + } + + public static String copyStreamToString(InputStream stream) throws IOException { + final StringBuilder sb = new StringBuilder(); + int read; + while ((read = stream.read()) != -1) + sb.append((char) read); + return sb.toString(); + } + + private final Logger log; + + private final InputStream in; + private final OutputStream out; + + private int bufSize = 1; + private boolean keepFlushing = true; + + private ErrorCallback errCB = new ErrorCallback() { + public void onError(IOException ioe) { + } + }; // Default null cb + + public StreamCopier(String name, InputStream in, OutputStream out) { + this.in = in; + this.out = out; + + setName("streamCopier"); + log = LoggerFactory.getLogger(name); + } + + public StreamCopier bufSize(int size) { + bufSize = size; + return this; + } + + public StreamCopier keepFlushing(boolean choice) { + keepFlushing = choice; + return this; + } + + public StreamCopier daemon(boolean choice) { + setDaemon(choice); + return this; + } + + public StreamCopier errorCallback(ErrorCallback errCB) { + this.errCB = errCB; + return this; + } + + @Override + public void run() { + try { + log.debug("Wil pipe from {} to {}", in, out); + copy(in, out, bufSize, keepFlushing); + log.debug("EOF on {}", in); + } catch (IOException ioe) { + log.error("In pipe from {} to {}: " + ioe.toString(), in, out); + errCB.onError(ioe); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/Connection.java b/src/main/java/net/schmizz/sshj/connection/Connection.java new file mode 100644 index 00000000..5906a16a --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/Connection.java @@ -0,0 +1,150 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection; + +import net.schmizz.concurrent.Future; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.channel.Channel; +import net.schmizz.sshj.connection.channel.OpenFailException; +import net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; + +/** + * Connection layer of the SSH protocol. + * + * @see rfc4254 + */ +public interface Connection { + + /** + * Attach a {@link net.schmizz.sshj.connection.channel.Channel} to this connection. A channel must be attached to + * the connection if it is to receive any channel-specific data that is received. + */ + void attach(Channel chan); + + /** + * Attach a {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener} to this connection, which + * will be delegated opening of any {@code CHANNEL_OPEN} packets {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener#getChannelType() + * for which it is responsible}. + */ + void attach(ForwardedChannelOpener opener); + + /** Forget an attached {@link Channel}. */ + void forget(Channel chan); + + /** Forget an attached {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener}. */ + void forget(ForwardedChannelOpener handler); + + /** Returns an attached {@link Channel} of specified channel-id, or {@code null} if no such channel was attached */ + Channel get(int id); + + /** Wait for the situation that no channels are attached (e.g., got closed). */ + void join() throws InterruptedException; + + /** + * Returns an attached {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener} of specified + * channel-type, or {@code null} if no such channel was attached + */ + ForwardedChannelOpener get(String chanType); + + /** Returns an available ID a {@link net.schmizz.sshj.connection.channel.Channel} can rightfully claim. */ + int nextID(); + + /** + * Send an SSH global request. + * + * @param name request name + * @param wantReply whether a reply is requested + * @param specifics {@link net.schmizz.sshj.common.SSHPacket} containing fields specific to the request + * + * @return a {@link net.schmizz.concurrent.Future} for the reply data (in case {@code wantReply} is true) which + * allows waiting on the reply, or {@code null} if a reply is not requested. + * + * @throws TransportException if there is an error sending the request + */ + public Future sendGlobalRequest(String name, boolean wantReply, + Buffer.PlainBuffer specifics) throws TransportException; + + /** + * Send a {@code SSH_MSG_OPEN_FAILURE} for specified {@code Reason} and {@code message}. + * + * @param recipient + * @param reason + * @param message + * + * @throws TransportException + */ + void sendOpenFailure(int recipient, OpenFailException.Reason reason, String message) throws TransportException; + + /** + * Get the maximum packet size for the local window this connection recommends to any {@link Channel}'s that ask for + * it. + */ + int getMaxPacketSize(); + + /** + * Set the maximum packet size for the local window this connection recommends to any {@link Channel}'s that ask for + * it. + */ + void setMaxPacketSize(int maxPacketSize); + + /** + * Get the size for the local window this connection recommends to any {@link net.schmizz.sshj.connection.channel.Channel}'s + * that ask for it. + */ + int getWindowSize(); + + /** Set the size for the local window this connection recommends to any {@link Channel}'s that ask for it. */ + void setWindowSize(int windowSize); + + /** Get the associated {@link Transport}. */ + Transport getTransport(); + + /** + * Get the {@code timeout} this connection uses for blocking operations and recommends to any {@link Channel other} + * {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener classes} that ask for it. + */ + int getTimeout(); + + /** + * Set the {@code timeout} this connection uses for blocking operations and recommends to any {@link + * net.schmizz.sshj.connection.channel.Channel other} {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener + * classes} that ask for it. + */ + void setTimeout(int timeout); +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/ConnectionException.java b/src/main/java/net/schmizz/sshj/connection/ConnectionException.java new file mode 100644 index 00000000..b3e1afd3 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/ConnectionException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection; + +import net.schmizz.concurrent.ExceptionChainer; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHException; + +/** Connection-layer exception. */ +public class ConnectionException extends SSHException { + + public static final ExceptionChainer chainer = new ExceptionChainer() { + public ConnectionException chain(Throwable t) { + if (t instanceof ConnectionException) + return (ConnectionException) t; + else + return new ConnectionException(t); + } + }; + + public ConnectionException() { + super(); + } + + public ConnectionException(DisconnectReason code) { + super(code); + } + + public ConnectionException(DisconnectReason code, String message) { + super(code, message); + } + + public ConnectionException(DisconnectReason code, String message, Throwable cause) { + super(code, message, cause); + } + + public ConnectionException(DisconnectReason code, Throwable cause) { + super(code, cause); + } + + public ConnectionException(String message) { + super(message); + } + + public ConnectionException(String message, Throwable cause) { + super(message, cause); + } + + public ConnectionException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/ConnectionProtocol.java b/src/main/java/net/schmizz/sshj/connection/ConnectionProtocol.java new file mode 100644 index 00000000..1daccef1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/ConnectionProtocol.java @@ -0,0 +1,234 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection; + +import net.schmizz.concurrent.Future; +import net.schmizz.concurrent.FutureUtils; +import net.schmizz.sshj.AbstractService; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.ErrorNotifiable; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.channel.Channel; +import net.schmizz.sshj.connection.channel.OpenFailException; +import net.schmizz.sshj.connection.channel.OpenFailException.Reason; +import net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; + +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** {@link Connection} implementation. */ +public class ConnectionProtocol extends AbstractService implements Connection { + + private final Object internalSynchronizer = new Object(); + + private final AtomicInteger nextID = new AtomicInteger(); + + private final Map channels = new ConcurrentHashMap(); + + private final Map openers = new ConcurrentHashMap(); + + private final Queue> globalReqFutures = new LinkedList>(); + + private int windowSize = 2048 * 1024; + private int maxPacketSize = 32 * 1024; + + /** + * Create with an associated {@link Transport}. + * + * @param trans transport layer + */ + public ConnectionProtocol(Transport trans) { + super("ssh-connection", trans); + } + + public void attach(Channel chan) { + log.info("Attaching `{}` channel (#{})", chan.getType(), chan.getID()); + channels.put(chan.getID(), chan); + } + + public Channel get(int id) { + return channels.get(id); + } + + public ForwardedChannelOpener get(String chanType) { + return openers.get(chanType); + } + + public void forget(Channel chan) { + log.info("Forgetting `{}` channel (#{})", chan.getType(), chan.getID()); + channels.remove(chan.getID()); + synchronized (internalSynchronizer) { + if (channels.isEmpty()) + internalSynchronizer.notifyAll(); + } + } + + public void forget(ForwardedChannelOpener opener) { + log.info("Forgetting opener for `{}` channels: {}", opener.getChannelType(), opener); + openers.remove(opener.getChannelType()); + } + + public void attach(ForwardedChannelOpener opener) { + log.info("Attaching opener for `{}` channels: {}", opener.getChannelType(), opener); + openers.put(opener.getChannelType(), opener); + } + + private Channel getChannel(SSHPacket buffer) throws ConnectionException { + int recipient = buffer.readInt(); + Channel channel = get(recipient); + if (channel != null) + return channel; + else { + buffer.rpos(buffer.rpos() - 5); + throw new ConnectionException(DisconnectReason.PROTOCOL_ERROR, "Received " + buffer.readMessageID() + + " on unknown channel #" + recipient); + } + } + + @Override + public void handle(Message msg, SSHPacket buf) throws SSHException { + if (msg.in(91, 100)) + getChannel(buf).handle(msg, buf); + + else if (msg.in(80, 90)) + switch (msg) { + case REQUEST_SUCCESS: + gotGlobalReqResponse(buf); + break; + case REQUEST_FAILURE: + gotGlobalReqResponse(null); + break; + case CHANNEL_OPEN: + gotChannelOpen(buf); + break; + default: + super.handle(msg, buf); + } + + else + super.handle(msg, buf); + } + + @Override + public void notifyError(SSHException error) { + super.notifyError(error); + + synchronized (globalReqFutures) { + FutureUtils.alertAll(error, globalReqFutures); + globalReqFutures.clear(); + } + + ErrorNotifiable.Util.alertAll(error, channels.values()); + channels.clear(); + } + + public int getMaxPacketSize() { + return maxPacketSize; + } + + public Transport getTransport() { + return trans; + } + + public void setMaxPacketSize(int maxPacketSize) { + this.maxPacketSize = maxPacketSize; + } + + public int getWindowSize() { + return windowSize; + } + + public void setWindowSize(int windowSize) { + this.windowSize = windowSize; + } + + public void join() throws InterruptedException { + synchronized (internalSynchronizer) { + while (!channels.isEmpty()) + internalSynchronizer.wait(); + } + } + + public int nextID() { + return nextID.getAndIncrement(); + } + + public Future sendGlobalRequest(String name, boolean wantReply, + Buffer.PlainBuffer specifics) throws TransportException { + synchronized (globalReqFutures) { + log.info("Making global request for `{}`", name); + trans.write(new SSHPacket(Message.GLOBAL_REQUEST) // + .putString(name) // + .putBoolean(wantReply) // + .putBuffer(specifics)); // + + Future future = null; + if (wantReply) { + future = new Future("global req for " + name, ConnectionException.chainer); + globalReqFutures.add(future); + } + return future; + } + } + + private void gotGlobalReqResponse(SSHPacket response) throws ConnectionException { + synchronized (globalReqFutures) { + Future gr = globalReqFutures.poll(); + if (gr == null) + throw new ConnectionException(DisconnectReason.PROTOCOL_ERROR, + "Got a global request response when none was requested"); + else if (response == null) + gr.error(new ConnectionException("Global request [" + gr + "] failed")); + else + gr.set(response); + } + } + + private void gotChannelOpen(SSHPacket buf) throws ConnectionException, TransportException { + final String type = buf.readString(); + log.debug("Received CHANNEL_OPEN for `{}` channel", type); + if (openers.containsKey(type)) + openers.get(type).handleOpen(buf); + else { + log.warn("No opener found for `{}` CHANNEL_OPEN request -- rejecting", type); + sendOpenFailure(buf.readInt(), OpenFailException.Reason.UNKNOWN_CHANNEL_TYPE, ""); + } + } + + public void sendOpenFailure(int recipient, Reason reason, String message) throws TransportException { + trans.write(new SSHPacket(Message.CHANNEL_OPEN_FAILURE) // + .putInt(recipient) // + .putInt(reason.getCode()) // + .putString(message)); + } + + @Override + public void notifyDisconnect() throws SSHException { + super.notifyDisconnect(); + // wh'about them futures? + for (Channel chan : channels.values()) + chan.close(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java b/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java new file mode 100644 index 00000000..f0d2094d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java @@ -0,0 +1,382 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel; + +import net.schmizz.concurrent.Event; +import net.schmizz.concurrent.FutureUtils; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +public abstract class AbstractChannel implements Channel { + + /** Logger */ + protected final Logger log; + + /** Transport layer */ + protected final Transport trans; + /** Connection layer */ + protected final Connection conn; + + /** Channel type */ + private final String type; + /** Channel ID */ + private final int id; + /** Remote recipient ID */ + private int recipient; + + private final Queue> chanReqResponseEvents = new LinkedList>(); + + /* The lock used by #newEvent to create open & close events */ + private final ReentrantLock lock = new ReentrantLock(); + /** Channel open event */ + protected final Event open; + /** Channel close event */ + private final Event close; + + /* Access to these fields should be synchronized using this object */ + private boolean eofSent; + private boolean eofGot; + private boolean closeRequested; + + /** Local window */ + protected final Window.Local lwin; + /** stdout stream */ + private final ChannelInputStream in; + + /** Remote window */ + protected Window.Remote rwin; + /** stdin stream */ + private ChannelOutputStream out; + + private volatile boolean autoExpand = false; + + protected AbstractChannel(String type, Connection conn) { + this.type = type; + this.conn = conn; + this.trans = conn.getTransport(); + + id = conn.nextID(); + + log = LoggerFactory.getLogger("chan#" + id); + + lwin = new Window.Local(id, conn.getWindowSize(), conn.getMaxPacketSize()); + in = new ChannelInputStream(this, trans, lwin); + + open = new Event("chan#" + id + " / " + "open", ConnectionException.chainer, lock); + close = new Event("chan#" + id + " / " + "close", ConnectionException.chainer, lock); + } + + protected void init(int recipient, int remoteWinSize, int remoteMaxPacketSize) { + this.recipient = recipient; + rwin = new Window.Remote(id, remoteWinSize, remoteMaxPacketSize); + out = new ChannelOutputStream(this, trans, rwin); + log.info("Initialized - {}", this); + } + + public boolean getAutoExpand() { + return autoExpand; + } + + public int getID() { + return id; + } + + public InputStream getInputStream() { + return in; + } + + public int getLocalMaxPacketSize() { + return lwin.getMaxPacketSize(); + } + + public int getLocalWinSize() { + return lwin.getSize(); + } + + public OutputStream getOutputStream() { + return out; + } + + public int getRecipient() { + return recipient; + } + + public int getRemoteMaxPacketSize() { + return rwin.getMaxPacketSize(); + } + + public int getRemoteWinSize() { + return rwin.getSize(); + } + + public String getType() { + return type; + } + + public void handle(Message msg, SSHPacket buf) throws ConnectionException, TransportException { + switch (msg) { + + case CHANNEL_DATA: + receiveInto(in, buf); + break; + + case CHANNEL_EXTENDED_DATA: + gotExtendedData(buf.readInt(), buf); + break; + + case CHANNEL_WINDOW_ADJUST: + gotWindowAdjustment(buf.readInt()); + break; + + case CHANNEL_REQUEST: + gotChannelRequest(buf); + break; + + case CHANNEL_SUCCESS: + gotResponse(true); + break; + + case CHANNEL_FAILURE: + gotResponse(false); + break; + + case CHANNEL_EOF: + gotEOF(); + break; + + case CHANNEL_CLOSE: + gotClose(); + break; + + default: + gotUnknown(msg, buf); + + } + } + + private void gotClose() throws TransportException { + log.info("Got close"); + try { + closeAllStreams(); + sendClose(); + } finally { + finishOff(); + } + } + + /** Called when all I/O streams should be closed. Subclasses can override but must call super. */ + protected void closeAllStreams() { + IOUtils.closeQuietly(in, out); + } + + public void notifyError(SSHException error) { + log.debug("Channel #{} got notified of {}", getID(), error.toString()); + + FutureUtils.alertAll(error, open, close); + FutureUtils.alertAll(error, chanReqResponseEvents); + + in.notifyError(error); + out.notifyError(error); + + finishOff(); + } + + public void setAutoExpand(boolean autoExpand) { + this.autoExpand = autoExpand; + } + + public void close() throws ConnectionException, TransportException { + lock.lock(); + try { + try { + sendClose(); + } catch (TransportException e) { + if (!close.hasError()) + throw e; + } + close.await(conn.getTimeout(), TimeUnit.SECONDS); + } finally { + lock.unlock(); + } + } + + protected synchronized void sendClose() throws TransportException { + try { + if (!closeRequested) { + log.info("Sending close"); + trans.write(newBuffer(Message.CHANNEL_CLOSE)); + } + } finally { + closeRequested = true; + } + } + + public synchronized boolean isOpen() { + lock.lock(); + try { + return open.isSet() && !close.isSet() && !closeRequested; + } finally { + lock.unlock(); + } + } + + private void gotChannelRequest(SSHPacket buf) throws ConnectionException, TransportException { + final String reqType = buf.readString(); + buf.readBoolean(); // We don't care about the 'want-reply' value + log.info("Got chan request for `{}`", reqType); + handleRequest(reqType, buf); + } + + private void gotWindowAdjustment(int howMuch) { + log.info("Received window adjustment for {} bytes", howMuch); + rwin.expand(howMuch); + } + + /** Called when this channel's end-of-life has been reached. Subclasses may override but must call super. */ + protected void finishOff() { + conn.forget(this); + close.set(); + } + + protected void gotExtendedData(int dataTypeCode, SSHPacket buf) throws ConnectionException, TransportException { + throw new ConnectionException(DisconnectReason.PROTOCOL_ERROR, "Extended data not supported on " + type + + " channel"); + } + + protected void gotUnknown(Message msg, SSHPacket buf) throws ConnectionException, TransportException { + } + + protected void handleRequest(String reqType, SSHPacket buf) throws ConnectionException, TransportException { + trans.write(newBuffer(Message.CHANNEL_FAILURE)); + } + + protected SSHPacket newBuffer(Message cmd) { + return new SSHPacket(cmd).putInt(recipient); + } + + protected void receiveInto(ChannelInputStream stream, SSHPacket buf) throws ConnectionException, TransportException { + final int len = buf.readInt(); + if (len < 0 || len > getLocalMaxPacketSize() || len != buf.available()) + throw new ConnectionException(DisconnectReason.PROTOCOL_ERROR, "Bad item length: " + len); + if (log.isTraceEnabled()) + log.trace("IN #{}: {}", id, ByteArrayUtils.printHex(buf.array(), buf.rpos(), len)); + stream.receive(buf.array(), buf.rpos(), len); + } + + protected synchronized Event sendChannelRequest(String reqType, boolean wantReply, + Buffer.PlainBuffer reqSpecific) throws TransportException { + log.info("Sending channel request for `{}`", reqType); + trans.write( + newBuffer(Message.CHANNEL_REQUEST) + .putString(reqType) + .putBoolean(wantReply) + .putBuffer(reqSpecific) + ); + + Event responseEvent = null; + if (wantReply) { + responseEvent = new Event("chan#" + id + " / " + "chanreq for " + reqType, ConnectionException.chainer, lock); + chanReqResponseEvents.add(responseEvent); + } + return responseEvent; + } + + private synchronized void gotResponse(boolean success) throws ConnectionException { + final Event responseEvent = chanReqResponseEvents.poll(); + if (responseEvent != null) { + if (success) + responseEvent.set(); + else + responseEvent.error(new ConnectionException("Request failed")); + } else + throw new ConnectionException( + DisconnectReason.PROTOCOL_ERROR, + "Received response to channel request when none was requested"); + } + + private synchronized void gotEOF() throws TransportException { + log.info("Got EOF"); + eofGot = true; + eofInputStreams(); + if (eofSent) + sendClose(); + } + + /** Called when EOF has been received. Subclasses can override but must call super. */ + protected void eofInputStreams() { + in.eof(); + } + + public synchronized void sendEOF() throws TransportException { + try { + if (!closeRequested && !eofSent) { + log.info("Sending EOF"); + trans.write(newBuffer(Message.CHANNEL_EOF)); + if (eofGot) + sendClose(); + } + } finally { + eofSent = true; + out.setClosed(); + } + } + + @Override + public String toString() { + return "< " + type + " channel: id=" + id + ", recipient=" + recipient + ", localWin=" + lwin + ", remoteWin=" + + rwin + " >"; + } + + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/Channel.java b/src/main/java/net/schmizz/sshj/connection/channel/Channel.java new file mode 100644 index 00000000..87621510 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/Channel.java @@ -0,0 +1,148 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel; + +import net.schmizz.sshj.common.ErrorNotifiable; +import net.schmizz.sshj.common.SSHPacketHandler; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.TransportException; + +import java.io.Closeable; +import java.io.InputStream; +import java.io.OutputStream; + +/** A channel is the basic medium for application-layer data on top of an SSH transport. */ +public interface Channel extends Closeable, SSHPacketHandler, ErrorNotifiable { + + /** Direct channels are those that are initiated by us. */ + interface Direct extends Channel { + /** + * Request opening this channel from remote end. + * + * @throws OpenFailException in case the channel open request was rejected + * @throws net.schmizz.sshj.connection.ConnectionException + * other connection-layer error + * @throws TransportException error writing packets etc. + */ + void open() throws OpenFailException, ConnectionException, TransportException; + + } + + /** Forwarded channels are those that are initiated by the server. */ + interface Forwarded extends Channel { + + /** + * Confirm {@code CHANNEL_OPEN} request. + * + * @throws TransportException error sending confirmation packet + */ + void confirm() throws TransportException; + + /** Returns the IP of where the forwarded connection originates. */ + String getOriginatorIP(); + + /** Returns port from which the forwarded connection originates. */ + int getOriginatorPort(); + + /** + * Indicate rejection to remote end. + * + * @param reason indicate {@link OpenFailException.Reason reason} for rejection of the request + * @param message indicate a message for why the request is rejected + * + * @throws TransportException error sending rejection packet + */ + void reject(OpenFailException.Reason reason, String message) throws TransportException; + + } + + + /** Close this channel. */ + void close() throws TransportException, ConnectionException; + + /** + * Returns whether auto-expansion of local window is set. + * + * @see #setAutoExpand(boolean) + */ + boolean getAutoExpand(); + + /** Returns the channel ID */ + int getID(); + + /** Returns the {@code InputStream} for this channel. */ + InputStream getInputStream(); + + /** Returns the maximum packet size that we have specified. */ + int getLocalMaxPacketSize(); + + /** Returns the current local window size. */ + int getLocalWinSize(); + + /** Returns an {@code OutputStream} for this channel. */ + OutputStream getOutputStream(); + + /** Returns the channel ID at the remote end. */ + int getRecipient(); + + /** Returns the maximum packet size as specified by the remote end. */ + int getRemoteMaxPacketSize(); + + /** Returns the current remote window size. */ + int getRemoteWinSize(); + + /** Returns the channel type identifier. */ + String getType(); + + /** Returns whether the channel is open. */ + boolean isOpen(); + + /** + * Sends an EOF message to the server for this channel; indicating that no more data will be sent by us. The {@code + * OutputStream} for this channel will be closed and no longer usable. + */ + void sendEOF() throws TransportException; + + /** + * Set whether local window should automatically expand when data is received, irrespective of whether data has been + * read from that stream. This is useful e.g. when a remote command produces a lot of output that would fill the + * local window but you are not interested in reading from its {@code InputStream}. + * + * @param autoExpand + */ + void setAutoExpand(boolean autoExpand); + +} diff --git a/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java b/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java new file mode 100644 index 00000000..572ef769 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java @@ -0,0 +1,171 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel; + +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.ErrorNotifiable; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; + +/** + * {@link InputStream} for channels. Can {@link #receive(byte[], int, int) receive} data into its buffer for serving to + * readers. + */ +public final class ChannelInputStream extends InputStream implements ErrorNotifiable { + + private final Logger log; + + private final Channel chan; + private final Transport trans; + private final Window.Local win; + private final Buffer.PlainBuffer buf; + private final byte[] b = new byte[1]; + + private boolean eof; + private SSHException error; + + public ChannelInputStream(Channel chan, Transport trans, Window.Local win) { + log = LoggerFactory.getLogger("<< chan#" + chan.getID() + " / input stream >>"); + + this.chan = chan; + this.trans = trans; + this.win = win; + buf = new Buffer.PlainBuffer(chan.getLocalMaxPacketSize()); + } + + @Override + public int available() { + synchronized (buf) { + return buf.available(); + } + } + + @Override + public void close() { + eof(); + } + + public void eof() { + synchronized (buf) { + if (!eof) { + eof = true; + buf.notifyAll(); + } + } + } + + public synchronized void notifyError(SSHException error) { + this.error = error; + eof(); + } + + @Override + public int read() throws IOException { + synchronized (b) { + return read(b, 0, 1) == -1 ? -1 : b[0]; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + synchronized (buf) { + for (; ;) { + if (buf.available() > 0) + break; + if (eof) + if (error != null) + throw error; + else + return -1; + try { + buf.wait(); + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + if (len > buf.available()) + len = buf.available(); + buf.readRawBytes(b, off, len); + if (buf.rpos() > win.getMaxPacketSize() && buf.available() == 0) + buf.clear(); + } + + if (!chan.getAutoExpand()) + checkWindow(); + + return len; + } + + public void receive(byte[] data, int offset, int len) throws ConnectionException, TransportException { + if (eof) + throw new ConnectionException("Getting data on EOF'ed stream"); + synchronized (buf) { + buf.putRawBytes(data, offset, len); + buf.notifyAll(); + } + win.consume(len); + if (chan.getAutoExpand()) + checkWindow(); + } + + private void checkWindow() throws TransportException { + synchronized (win) { + final int adjustment = win.neededAdjustment(); + if (adjustment > 0) { + log.info("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST to #{} for {} bytes", chan.getRecipient(), adjustment); + trans.write(new SSHPacket(Message.CHANNEL_WINDOW_ADJUST).putInt(chan.getRecipient()).putInt(adjustment)); + win.expand(adjustment); + } + } + } + + @Override + public String toString() { + return "< ChannelInputStream for Channel #" + chan.getID() + " >"; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java b/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java new file mode 100644 index 00000000..dce78d40 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/ChannelOutputStream.java @@ -0,0 +1,158 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel; + +import net.schmizz.sshj.common.ErrorNotifiable; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.Transport; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * {@link OutputStream} for channels. Buffers data upto the remote window's maximum packet size. Data can also be + * flushed via {@link #flush()} and is also flushed on {@link #close()}. + */ +public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable { + + private final Channel chan; + private Transport trans; + private final Window.Remote win; + private final SSHPacket buffer = new SSHPacket(); + private final byte[] b = new byte[1]; + private int bufferLength; + private boolean closed; + private SSHException error; + + public ChannelOutputStream(Channel chan, Transport trans, Window.Remote win) { + this.chan = chan; + this.trans = trans; + this.win = win; + prepBuffer(); + } + + private void prepBuffer() { + bufferLength = 0; + buffer.rpos(5); + buffer.wpos(5); + buffer.putMessageID(Message.CHANNEL_DATA); + buffer.putInt(0); // meant to be recipient + buffer.putInt(0); // meant to be data length + } + + @Override + public synchronized void write(int w) throws IOException { + b[0] = (byte) w; + write(b, 0, 1); + } + + @Override + public synchronized void write(byte[] data, int off, int len) throws IOException { + checkClose(); + while (len > 0) { + final int x = Math.min(len, win.getMaxPacketSize() - bufferLength); + if (x <= 0) { + flush(); + continue; + } + buffer.putRawBytes(data, off, x); + bufferLength += x; + off += x; + len -= x; + } + } + + public synchronized void notifyError(SSHException error) { + this.error = error; + } + + private synchronized void checkClose() throws SSHException { + if (closed) + if (error != null) + throw error; + else + throw new ConnectionException("Stream closed"); + } + + @Override + public synchronized void close() throws IOException { + if (!closed) + try { + flush(); + chan.sendEOF(); + } finally { + setClosed(); + } + } + + public synchronized void setClosed() { + closed = true; + } + + @Override + public synchronized void flush() throws IOException { + checkClose(); + + if (bufferLength <= 0) // No data to send + return; + + putRecipientAndLength(); + + try { + win.waitAndConsume(bufferLength); + trans.write(buffer); + } finally { + prepBuffer(); + } + } + + private void putRecipientAndLength() { + final int origPos = buffer.wpos(); + buffer.wpos(6); + buffer.putInt(chan.getRecipient()); + buffer.putInt(bufferLength); + buffer.wpos(origPos); + } + + @Override + public String toString() { + return "< ChannelOutputStream for Channel #" + chan.getID() + " >"; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/OpenFailException.java b/src/main/java/net/schmizz/sshj/connection/channel/OpenFailException.java new file mode 100644 index 00000000..496116ec --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/OpenFailException.java @@ -0,0 +1,100 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel; + +import net.schmizz.sshj.connection.ConnectionException; + +public class OpenFailException extends ConnectionException { + + public enum Reason { + UNKNOWN(0), ADMINISTRATIVELY_PROHIBITED(1), CONNECT_FAILED(2), UNKNOWN_CHANNEL_TYPE(3), RESOURCE_SHORTAGE(4); + + public static Reason fromInt(int code) { + for (Reason rc : Reason.values()) + if (rc.code == code) + return rc; + return UNKNOWN; + } + + private final int code; + + private Reason(int rc) { + this.code = rc; + } + + public int getCode() { + return code; + } + + } + + private final String channelType; + private final Reason reason; + private final String message; + + public OpenFailException(String channelType, int reasonCode, String message) { + super(message); + this.channelType = channelType; + this.reason = Reason.fromInt(reasonCode); + this.message = message; + } + + public OpenFailException(String channelType, Reason reason, String message) { + super(message); + this.channelType = channelType; + this.reason = reason; + this.message = message; + } + + public String getChannelType() { + return channelType; + } + + @Override + public String getMessage() { + return message; + } + + public Reason getReason() { + return reason; + } + + @Override + public String toString() { + return "Opening `" + channelType + "` channel failed: " + getMessage(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/Window.java b/src/main/java/net/schmizz/sshj/connection/channel/Window.java new file mode 100644 index 00000000..1d5e665e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/Window.java @@ -0,0 +1,113 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection.channel; + +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.TransportException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class Window { + + protected final Logger log; + + protected final Object lock = new Object(); + + protected final int maxPacketSize; + + protected int size; + + public Window(int chanID, String kindOfWindow, int initialWinSize, int maxPacketSize) { + log = LoggerFactory.getLogger("<< chan#" + chanID + " / " + kindOfWindow + " >>"); + size = initialWinSize; + this.maxPacketSize = maxPacketSize; + } + + public void expand(int inc) { + synchronized (lock) { + log.debug("Increasing by {} up to {}", inc, size); + size += inc; + lock.notifyAll(); + } + } + + public int getMaxPacketSize() { + return maxPacketSize; + } + + public int getSize() { + return size; + } + + public void consume(int dec) { + synchronized (lock) { + log.debug("Consuming by " + dec + " down to " + size); + size -= dec; + if (size < 0) + throw new SSHRuntimeException("Window consumed to below 0"); + } + } + + @Override + public String toString() { + return "[winSize=" + size + "]"; + } + + /** Controls how much data we can send before an adjustment notification from remote end is required. */ + public static final class Remote extends Window { + + public Remote(int chanID, int initialWinSize, int maxPacketSize) { + super(chanID, "remote win", initialWinSize, maxPacketSize); + } + + public void waitAndConsume(int howMuch) throws ConnectionException { + synchronized (lock) { + while (size < howMuch) { + log.debug("Waiting, need window space for {} bytes", howMuch); + try { + lock.wait(); + } catch (InterruptedException ie) { + throw new ConnectionException(ie); + } + } + consume(howMuch); + } + } + + } + + /** Controls how much data remote end can send before an adjustment notification from us is required. */ + public static final class Local extends Window { + + private final int initialSize; + private final int threshold; + + public Local(int chanID, int initialWinSize, int maxPacketSize) { + super(chanID, "local win", initialWinSize, maxPacketSize); + this.initialSize = initialWinSize; + threshold = Math.min(maxPacketSize * 20, initialSize / 4); + } + + public int neededAdjustment() throws TransportException { + synchronized (lock) { + return (size - threshold <= 0) ? (initialSize - size) : 0; + } + } + + } + +} diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/AbstractDirectChannel.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/AbstractDirectChannel.java new file mode 100644 index 00000000..85be1872 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/AbstractDirectChannel.java @@ -0,0 +1,101 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.connection.channel.AbstractChannel; +import net.schmizz.sshj.connection.channel.Channel; +import net.schmizz.sshj.connection.channel.OpenFailException; +import net.schmizz.sshj.transport.TransportException; + +import java.util.concurrent.TimeUnit; + +/** Base class for direct channels whose open is initated by the client. */ +public abstract class AbstractDirectChannel extends AbstractChannel implements Channel.Direct { + + protected AbstractDirectChannel(String name, Connection conn) { + super(name, conn); + + /* + * We expect to receive channel open confirmation/rejection and want to be able to next this packet. + */ + conn.attach(this); + } + + public void open() throws ConnectionException, TransportException { + trans.write(buildOpenReq()); + open.await(conn.getTimeout(), TimeUnit.SECONDS); + } + + private void gotOpenConfirmation(SSHPacket buf) { + init(buf.readInt(), buf.readInt(), buf.readInt()); + open.set(); + } + + private void gotOpenFailure(SSHPacket buf) { + open.error(new OpenFailException(getType(), buf.readInt(), buf.readString())); + finishOff(); + } + + protected SSHPacket buildOpenReq() { + return new SSHPacket(Message.CHANNEL_OPEN) + .putString(getType()) + .putInt(getID()) + .putInt(getLocalWinSize()) + .putInt(getLocalMaxPacketSize()); + } + + @Override + protected void gotUnknown(Message cmd, SSHPacket buf) throws ConnectionException, TransportException { + switch (cmd) { + + case CHANNEL_OPEN_CONFIRMATION: + gotOpenConfirmation(buf); + break; + + case CHANNEL_OPEN_FAILURE: + gotOpenFailure(buf); + break; + + default: + super.gotUnknown(cmd, buf); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java new file mode 100644 index 00000000..a4744714 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.common.StreamCopier.ErrorCallback; +import net.schmizz.sshj.connection.Connection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ServerSocketFactory; +import java.io.Closeable; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; + +public class LocalPortForwarder { + + private class DirectTCPIPChannel extends AbstractDirectChannel { + + private final Socket sock; + + private DirectTCPIPChannel(Connection conn, Socket sock) { + super("direct-tcpip", conn); + this.sock = sock; + } + + private void start() throws IOException { + sock.setSendBufferSize(getLocalMaxPacketSize()); + sock.setReceiveBufferSize(getRemoteMaxPacketSize()); + + final ErrorCallback closer = StreamCopier.closeOnErrorCallback(this, + new Closeable() { + public void close() throws IOException { + sock.close(); + } + }); + + new StreamCopier("chan2soc", getInputStream(), sock.getOutputStream()) // + .bufSize(getLocalMaxPacketSize()) // + .errorCallback(closer) // + .daemon(true) // + .start(); + + new StreamCopier("soc2chan", sock.getInputStream(), getOutputStream()) // + .bufSize(getRemoteMaxPacketSize()) // + .errorCallback(closer) // + .daemon(true) // + .start(); + } + + @Override + protected SSHPacket buildOpenReq() { + return super.buildOpenReq() // + .putString(host) // + .putInt(port) // + .putString(ss.getInetAddress().getHostAddress()) // + .putInt(ss.getLocalPort()); + } + + } + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final Connection conn; + private final ServerSocket ss; + private final String host; + private final int port; + + public LocalPortForwarder(Connection conn, SocketAddress listeningAddr, String host, int port) throws IOException { + this(ServerSocketFactory.getDefault(), conn, listeningAddr, host, port); + } + + /** + * Create a local port forwarder with specified binding ({@code listeningAddr}. It does not, however, start + * listening unless {@link #listen() explicitly told to}. + * + * @param conn {@link Connection} implementation + * @param listeningAddr {@link SocketAddress} this forwarder will listen on, if {@code null} then an ephemeral port + * and valid local address will be picked to bind the server socket + * @param host what host the SSH server will further forward to + * @param port port on {@code toHost} + * + * @throws IOException if there is an error binding on specified {@code listeningAddr} + */ + public LocalPortForwarder(ServerSocketFactory ssf, Connection conn, SocketAddress listeningAddr, String host, int port) throws IOException { + this.conn = conn; + this.host = host; + this.port = port; + this.ss = ssf.createServerSocket(); + ss.setReceiveBufferSize(conn.getMaxPacketSize()); + ss.bind(listeningAddr); + } + + public SocketAddress getListeningAddress() { + return ss.getLocalSocketAddress(); + } + + /** Start listening for incoming connections and forward to remote host as a channel. */ + public void listen() throws IOException { + log.info("Listening on {}", ss.getLocalSocketAddress()); + Socket sock; + while (true) { + sock = ss.accept(); + log.info("Got connection from {}", sock.getRemoteSocketAddress()); + DirectTCPIPChannel chan = new DirectTCPIPChannel(conn, sock); + chan.open(); + chan.start(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/PTYMode.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/PTYMode.java new file mode 100644 index 00000000..8d0d01cb --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/PTYMode.java @@ -0,0 +1,179 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.common.Buffer; + +import java.util.Map; +import java.util.Map.Entry; + +/** Various modes for a psuedo-terminal. They are meant to have integer parameters. */ +public enum PTYMode { + /** + * Interrupt character; 255 if none. Similarly for the other characters. Not all of these characters are supported + * on all systems. + */ + VINTR(1), + /** The quit character (sends SIGQUIT signal on POSIX systems). */ + VQUIT(2), + /** Erase the character to left of the cursor. */ + VERASE(3), + /** Kill the current input line. */ + VKILL(4), + /** End-of-file character (sends EOF from the terminal). */ + VEOF(5), + /** End-of-line character in addition to carriage return and/or linefeed. */ + VEOL(6), + /** Additional end-of-line character. */ + VEOL2(7), + /** Continues paused output (normally control-Q). */ + VSTART(8), + /** Pauses output (normally control-S). */ + VSTOP(9), + /** Suspends the current program. */ + VSUSP(10), + /** Another suspend character. */ + VDSUSP(11), + /** Reprints the current input line. */ + VREPRINT(12), + /** Erases a word left of cursor. */ + VWERASE(13), + /** Enter the next character typed literally, even if it is a special character. */ + VLNEXT(14), + /** Character to flush output. */ + VFLUSH(15), + /** Switch to a different shell layer. */ + VSWTCH(16), + /** Prints system status line (load, command, pid, etc). */ + VSTATUS(17), + /** Toggles the flushing of terminal output. */ + VDISCARD(18), + /** The ignore parity flag. The parameter SHOULD be 0 if this flag is FALSE, and 1 if it is TRUE. */ + IGNPAR(30), + /** Mark parity and framing errors. */ + PARMRK(31), + /** Enable checking of parity errors. */ + INPCK(32), + /** Strip 8th bit off characters. */ + ISTRIP(33), + /** Map NL into CR on input. */ + INLCR(34), + /** Ignore CR on input. */ + IGNCR(35), + /** Map CR to NL on input. */ + ICRNL(36), + /** Translate uppercase characters to lowercase. */ + IUCLC(37), + /** Enable output flow control. */ + IXON(38), + /** Any char will restart after stop. */ + IXANY(39), + /** Enable input flow control. */ + IXOFF(40), + /** Ring bell on input queue full. */ + IMAXBEL(41), + /** Enable signals INTR, QUIT, [D]SUSP. */ + ISIG(50), + /** Canonicalize input lines. */ + ICANON(51), + /** Enable input and output of uppercase characters by preceding their lowercase equivalents with "\". */ + XCASE(52), + /** Enable echoing. */ + ECHO(53), + /** Visually erase chars. */ + ECHOE(54), + /** Kill character discards current line. */ + ECHOK(55), + /** Echo NL even if ECHO is off. */ + ECHONL(56), + /** Don't flush after interrupt. */ + NOFLSH(57), + /** Stop background jobs from output. */ + TOSTOP(58), + /** Enable extensions. */ + IEXTEN(59), + /** Echo control characters as ˆ(Char). */ + ECHOCTL(60), + /** Visual erase for line kill. */ + ECHOKE(61), + /** Retype pending input. */ + PENDIN(62), + /** Enable output processing. */ + OPOST(70), + /** Convert lowercase to uppercase. */ + OLCUC(71), + /** Map NL to CR-NL. */ + ONLCR(72), + /** Translate carriage return to newline (output). */ + OCRNL(73), + /** Translate newline to carriage return-newline (output). */ + ONOCR(74), + /** Newline performs a carriage return (output). */ + ONLRET(75), + /** 7 bit mode. */ + CS7(90), + /** 8 bit mode. */ + CS8(91), + /** Parity enable. */ + PARENB(92), + /** Odd parity, else even. */ + PARODD(93), + /** Specifies the input baud rate in bits per second. */ + TTY_OP_ISPEED(128), + /** Specifies the output baud rate in bits per second. */ + TTY_OP_OSPEED(129); + + public static byte[] encode(Map modes) { + Buffer.PlainBuffer buf = new Buffer.PlainBuffer(); + for (Entry entry : modes.entrySet()) { + buf.putByte(entry.getKey().getOpcode()); + buf.putInt(entry.getValue()); + } + buf.putByte((byte) 0); + return buf.getCompactData(); + } + + private final byte opcode; + + private PTYMode(int opcode) { + this.opcode = (byte) opcode; + } + + public byte getOpcode() { + return opcode; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/Session.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/Session.java new file mode 100644 index 00000000..c60f001d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/Session.java @@ -0,0 +1,248 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.connection.channel.Channel; +import net.schmizz.sshj.transport.TransportException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +/** + * A {@code session} channel provides for execution of a remote {@link Command command}, {@link Shell shell} or {@link + * Subsystem subsystem}. Before this requests like starting X11 forwarding, setting environment variables, allocating a + * PTY etc. can be made. + *

+ * It is not legal to reuse a {@code session} channel for more than one of command, shell, or subsystem. Once one of + * these has been started this instance's API is invalid and that of the {@link Command specific} {@link Shell targets} + * {@link Subsystem returned} should be used. + * + * @see Command + * @see Shell + * @see Subsystem + */ +public interface Session extends Channel { + + /** Command API. */ + interface Command extends Channel { + + /** + * Read from the command's {@code stderr} stream into a string (blocking). + * + * @return the commands {@code stderr} output as a string + * + * @throws IOException if error reading from the stream + */ + String getErrorAsString() throws IOException; + + /** Returns the command's {@code stderr} stream. */ + InputStream getErrorStream(); + + /** + * If the command exit violently {@link #getExitSignal() with a signal}, an error message would have been + * received and can be retrieved via this method. Otherwise, this method will return {@code null}. + */ + String getExitErrorMessage(); + + /** + * Returns the {@link Signal signal} if the command exit violently, or {@code null} if this information was not + * received. + */ + Signal getExitSignal(); + + /** + * Returns the exit status of the command if it was received, or {@code null} if this information was not + * received. + */ + Integer getExitStatus(); + + /** + * If the command exit violently {@link #getExitSignal() with a signal}, information about whether a core dump + * took place would have been received and can be retrieved via this method. Otherwise, this method will return + * {@code null}. + */ + Boolean getExitWasCoreDumped(); + + /** + * Read from the command's {@code stdout} stream into a string (blocking). + * + * @return the command's {@code stdout} output as a string + * + * @throws IOException if error reading from the stream + */ + String getOutputAsString() throws IOException; + + /** + * Send a signal to the remote command. + * + * @param signal the signal + * + * @throws TransportException if error sending the signal + */ + void signal(Signal signal) throws TransportException; + + } + + /** Shell API. */ + interface Shell extends Channel { + + /** + * Whether the client can do local flow control using {@code control-S} and {@code control-Q}. + * + * @return boolean value indicating whether 'client can do', or {@code null} if no such information was + * received + */ + Boolean canDoFlowControl(); + + /** + * Sends a window dimension change message. + * + * @param cols terminal width, columns + * @param rows terminal height, rows + * @param width terminal width, pixels + * @param height terminal height, pixels + * + * @throws TransportException + */ + void changeWindowDimensions(int cols, int rows, int width, int height) throws TransportException; + + /** Returns the shell's {@code stderr} stream. */ + InputStream getErrorStream(); + + /** + * Send a signal. + * + * @param signal the signal + * + * @throws TransportException if error sending the signal + */ + void signal(Signal signal) throws TransportException; + + } + + /** Subsystem API. */ + interface Subsystem extends Channel { + Integer getExitStatus(); + } + + /** + * Allocates a default PTY. The default PTY is {@code "vt100"} with the echo modes disabled. + * + * @throws net.schmizz.sshj.connection.ConnectionException + * + * @throws TransportException + */ + void allocateDefaultPTY() throws ConnectionException, TransportException; + + /** + * Allocate a psuedo-terminal for this session. + *

+ * {@code 0} dimension parameters will be ignored by the server. + * + * @param term {@code TERM} environment variable value (e.g., {@code vt100}) + * @param cols terminal width, cols (e.g., 80) + * @param rows terminal height, rows (e.g., 24) + * @param width terminal width, pixels (e.g., 640) + * @param height terminal height, pixels (e.g., 480) + * @param modes + * + * @throws ConnectionException + * @throws TransportException + */ + void allocatePTY(String term, int cols, int rows, int width, int height, Map modes) + throws ConnectionException, TransportException; + + /** + * Execute a remote command. + * + * @param command + * + * @return {@link Command} instance which should now be used + * + * @throws ConnectionException if the request to execute the command failed + * @throws TransportException if there is an error sending the request + */ + Command exec(String command) throws ConnectionException, TransportException; + + /** + * Request X11 forwarding. + * + * @param authProto X11 authentication protocol name + * @param authCookie X11 authentication cookie + * @param screen X11 screen number + * + * @throws ConnectionException if the request failed + * @throws TransportException if there was an error sending the request + */ + void reqX11Forwarding(String authProto, String authCookie, int screen) throws ConnectionException, + TransportException; + + /** + * Set an enviornment variable. + * + * @param name name of the variable + * @param value value to set + * + * @throws ConnectionException if the request failed + * @throws TransportException if there was an error sending the request + */ + void setEnvVar(String name, String value) throws ConnectionException, TransportException; + + /** + * Request a shell. + * + * @return {@link Shell} instance which should now be used + * + * @throws ConnectionException if the request failed + * @throws TransportException if there was an error sending the request + */ + Shell startShell() throws ConnectionException, TransportException; + + /** + * Request a subsystem. + * + * @param name subsystem name + * + * @return {@link Subsystem} instance which should now be used + * + * @throws ConnectionException if the request failed + * @throws TransportException if there was an error sending the request + */ + Subsystem startSubsystem(String name) throws ConnectionException, TransportException; + +} diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/SessionChannel.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/SessionChannel.java new file mode 100644 index 00000000..20d1b536 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/SessionChannel.java @@ -0,0 +1,219 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.connection.channel.ChannelInputStream; +import net.schmizz.sshj.transport.TransportException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** {@link Session} implementation. */ +public class + SessionChannel extends AbstractDirectChannel implements Session, Session.Command, Session.Shell, + Session.Subsystem { + + private Integer exitStatus; + + private Signal exitSignal; + private Boolean wasCoreDumped; + private String exitErrMsg; + + private Boolean canDoFlowControl; + + private ChannelInputStream err = new ChannelInputStream(this, trans, lwin); + + public SessionChannel(Connection conn) { + super("session", conn); + } + + public void allocateDefaultPTY() throws ConnectionException, TransportException { + // TODO FIXME (maybe?): These modes were originally copied from what SSHD was doing; + // and then the echo modes were set to 0 to better serve the PTY example. + // Not sure what default PTY modes should be. + final Map modes = new HashMap(); + modes.put(PTYMode.ISIG, 1); + modes.put(PTYMode.ICANON, 1); + modes.put(PTYMode.ECHO, 0); + modes.put(PTYMode.ECHOE, 0); + modes.put(PTYMode.ECHOK, 0); + modes.put(PTYMode.ECHONL, 0); + modes.put(PTYMode.NOFLSH, 0); + allocatePTY("vt100", 0, 0, 0, 0, modes); + } + + public void allocatePTY(String term, int cols, int rows, int width, int height, Map modes) + throws ConnectionException, TransportException { + sendChannelRequest( + "pty-req", + true, + new Buffer.PlainBuffer() + .putString(term) + .putInt(cols) + .putInt(rows) + .putInt(width) + .putInt(height) + .putBytes(PTYMode.encode(modes)) + ).await(conn.getTimeout(), TimeUnit.SECONDS); + } + + public Boolean canDoFlowControl() { + return canDoFlowControl; + } + + public void changeWindowDimensions(int cols, int rows, int width, int height) throws TransportException { + sendChannelRequest( + "pty-req", + false, + new Buffer.PlainBuffer() + .putInt(cols) + .putInt(rows) + .putInt(width) + .putInt(height) + ); + } + + public Command exec(String command) throws ConnectionException, TransportException { + log.info("Will request to exec `{}`", command); + sendChannelRequest("exec", true, new Buffer.PlainBuffer().putString(command)).await(conn.getTimeout(), TimeUnit.SECONDS); + return this; + } + + public String getErrorAsString() throws IOException { + return StreamCopier.copyStreamToString(err); + } + + public InputStream getErrorStream() { + return err; + } + + public String getExitErrorMessage() { + return exitErrMsg; + } + + public Signal getExitSignal() { + return exitSignal; + } + + public Integer getExitStatus() { + return exitStatus; + } + + public String getOutputAsString() throws IOException { + return StreamCopier.copyStreamToString(getInputStream()); + } + + @Override + public void handleRequest(String req, SSHPacket buf) throws ConnectionException, TransportException { + if ("xon-xoff".equals(req)) + canDoFlowControl = buf.readBoolean(); + else if ("exit-status".equals(req)) + exitStatus = buf.readInt(); + else if ("exit-signal".equals(req)) { + exitSignal = Signal.fromString(buf.readString()); + wasCoreDumped = buf.readBoolean(); // core dumped + exitErrMsg = buf.readString(); + sendClose(); + } else + super.handleRequest(req, buf); + } + + public void reqX11Forwarding(String authProto, String authCookie, int screen) throws ConnectionException, + TransportException { + sendChannelRequest( + "x11-req", + true, + new Buffer.PlainBuffer() + .putBoolean(false) + .putString(authProto) + .putString(authCookie) + .putInt(screen) + ).await(conn.getTimeout(), TimeUnit.SECONDS); + } + + public void setEnvVar(String name, String value) throws ConnectionException, TransportException { + sendChannelRequest("env", true, new Buffer.PlainBuffer().putString(name).putString(value)).await(conn.getTimeout(), TimeUnit.SECONDS); + } + + public void signal(Signal sig) throws TransportException { + sendChannelRequest("signal", false, new Buffer.PlainBuffer().putString(sig.toString())); + } + + public Shell startShell() throws ConnectionException, TransportException { + sendChannelRequest("shell", true, null).await(conn.getTimeout(), TimeUnit.SECONDS); + return this; + } + + public Subsystem startSubsystem(String name) throws ConnectionException, TransportException { + log.info("Will request `{}` subsystem", name); + sendChannelRequest("subsystem", true, new Buffer.PlainBuffer().putString(name)).await(conn.getTimeout(), TimeUnit.SECONDS); + return this; + } + + public Boolean getExitWasCoreDumped() { + return wasCoreDumped; + } + + @Override + protected void closeAllStreams() { + IOUtils.closeQuietly(err); + super.closeAllStreams(); + } + + @Override + protected void eofInputStreams() { + err.eof(); // also close the stderr stream + super.eofInputStreams(); + } + + @Override + protected void gotExtendedData(int dataTypeCode, SSHPacket buf) throws ConnectionException, TransportException { + if (dataTypeCode == 1) + receiveInto(err, buf); + else + super.gotExtendedData(dataTypeCode, buf); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/SessionFactory.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/SessionFactory.java new file mode 100644 index 00000000..46a80018 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/SessionFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection.channel.direct; + +import net.schmizz.sshj.common.SSHException; + +/** A factory interface for creating SSH {@link Session session channels}. */ +public interface SessionFactory { + /** + * Opens a {@code session} channel. The returned {@link Session} instance allows {@link Session#exec(String) + * executing a remote command}, {@link Session#startSubsystem(String) starting a subsystem}, or {@link + * Session#startShell() starting a shell}. + * + * @return the opened {@code session} channel + * + * @throws SSHException + * @see {@link Session} + */ + Session startSession() throws SSHException; + +} diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/Signal.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/Signal.java new file mode 100644 index 00000000..a3590a47 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/Signal.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.direct; + +/** Various signals that may be sent or received. The signals are from POSIX and simply miss the {@code "SIG_"} prefix. */ +public enum Signal { + + ABRT("ABRT"), ALRM("ALRM"), FPE("FPE"), HUP("HUP"), ILL("ILL"), INT("INT"), KILL("KILL"), PIPE("PIPE"), QUIT( + "QUIT"), SEGV("SEGV"), TERM("TERM"), USR1("USR1"), USR2("USR2"), UNKNOWN("UNKNOWN"); + + /** + * Create from the string representation used when the signal is received as part of an SSH packet. + * + * @param name name of the signal as received + * + * @return the enum constant inferred + */ + public static Signal fromString(String name) { + for (Signal sig : Signal.values()) + if (sig.name.equals(name)) + return sig; + return UNKNOWN; + } + + private final String name; + + private Signal(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/AbstractForwardedChannel.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/AbstractForwardedChannel.java new file mode 100644 index 00000000..a4f69912 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/AbstractForwardedChannel.java @@ -0,0 +1,87 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.channel.AbstractChannel; +import net.schmizz.sshj.connection.channel.Channel; +import net.schmizz.sshj.connection.channel.OpenFailException.Reason; +import net.schmizz.sshj.transport.TransportException; + +/** Base class for forwarded channels whose open is initiated by the server. */ +public abstract class AbstractForwardedChannel extends AbstractChannel implements Channel.Forwarded { + + protected final String origIP; + protected final int origPort; + + /* + * First 2 args are standard; the others can be parsed from a CHANNEL_OPEN packet. + */ + + protected AbstractForwardedChannel(String type, Connection conn, int recipient, int remoteWinSize, + int remoteMaxPacketSize, String origIP, int origPort) { + super(type, conn); + this.origIP = origIP; + this.origPort = origPort; + init(recipient, remoteWinSize, remoteMaxPacketSize); + } + + public void confirm() throws TransportException { + log.info("Confirming `{}` channel #{}", getType(), getID()); + // Must ensure channel is attached before confirming, data could start coming in immediately! + conn.attach(this); + trans.write(newBuffer(Message.CHANNEL_OPEN_CONFIRMATION) + .putInt(getID()) + .putInt(getLocalWinSize()) + .putInt(getLocalMaxPacketSize())); + open.set(); + } + + public void reject(Reason reason, String message) throws TransportException { + log.info("Rejecting `{}` channel: {}", getType(), message); + conn.sendOpenFailure(getRecipient(), reason, message); + } + + public String getOriginatorIP() { + return origIP; + } + + public int getOriginatorPort() { + return origPort; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/AbstractForwardedChannelOpener.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/AbstractForwardedChannelOpener.java new file mode 100644 index 00000000..75436855 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/AbstractForwardedChannelOpener.java @@ -0,0 +1,97 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.channel.Channel; +import net.schmizz.sshj.connection.channel.OpenFailException; +import net.schmizz.sshj.transport.TransportException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** Base class for {@link net.schmizz.sshj.connection.channel.forwarded.ForwardedChannelOpener}'s. */ +public abstract class AbstractForwardedChannelOpener implements ForwardedChannelOpener { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final String chanType; + protected final Connection conn; + + protected AbstractForwardedChannelOpener(String chanType, Connection conn) { + this.chanType = chanType; + this.conn = conn; + } + + public String getChannelType() { + return chanType; + } + + /* + * Calls the listener with the new channel in a separate thread. + */ + + protected void callListener(final ConnectListener listener, final Channel.Forwarded chan) { + new Thread() { + + { + setName("ConnectListener"); + } + + @Override + public void run() { + try { + listener.gotConnect(chan); + } catch (IOException logged) { + log.warn("In callback to {}: {}", listener, logged); + if (chan.isOpen()) + IOUtils.closeQuietly(chan); + else + try { + chan.reject(OpenFailException.Reason.CONNECT_FAILED, ""); + } catch (TransportException cantdonthn) { + log.warn("Error rejecting {}: {}", chan, cantdonthn); + } + } + } + + }.start(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/ConnectListener.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/ConnectListener.java new file mode 100644 index 00000000..d3c39498 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/ConnectListener.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.connection.channel.Channel; + +import java.io.IOException; + +/** A connect listener is just that: it listens for new forwarded channels and can be delegated charge of them. */ +public interface ConnectListener { + + /** + * Notify this listener of a new forwarded channel. An implementation should firstly {@link + * net.schmizz.sshj.connection.channel.Channel.Forwarded#confirm() confirm} or {@link + * net.schmizz.sshj.connection.channel.Channel.Forwarded#reject() reject} that channel. + * + * @param chan the {@link net.schmizz.sshj.connection.channel.Channel.Forwarded forwarded channel} + * + * @throws java.io.IOException + */ + void gotConnect(Channel.Forwarded chan) throws IOException; + +} diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/ForwardedChannelOpener.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/ForwardedChannelOpener.java new file mode 100644 index 00000000..b200c168 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/ForwardedChannelOpener.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.TransportException; + +/** Takes care of handling {@code SSH_MSG_CHANNEL_OPEN} requests for forwarded channels of a specific type. */ +public interface ForwardedChannelOpener { + + /** Returns the name of the channel type this opener can next. */ + String getChannelType(); + + /** + * Delegates a {@code SSH_MSG_CHANNEL_OPEN} request for the channel type claimed by this opener. + * + * @param buf {@link net.schmizz.sshj.common.SSHPacket} containg the request except for the message identifier and + * channel type field + */ + void handleOpen(SSHPacket buf) throws ConnectionException, TransportException; + +} diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/RemotePortForwarder.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/RemotePortForwarder.java new file mode 100644 index 00000000..79c2f186 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/RemotePortForwarder.java @@ -0,0 +1,236 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.connection.channel.OpenFailException; +import net.schmizz.sshj.transport.TransportException; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** Handles remote port forwarding. */ +public class RemotePortForwarder extends AbstractForwardedChannelOpener { + + /** + * Represents a particular forwarding. From RFC 4254, s. 7.1 + *

+ *

+     *    The 'address to bind' and 'port number to bind' specify the IP
+     *    address (or domain name) and port on which connections for forwarding
+     *    are to be accepted.  Some strings used for 'address to bind' have
+     *    special-case semantics.
+     * 

+ * o "" means that connections are to be accepted on all protocol + * families supported by the SSH implementation. + *

+ * o "0.0.0.0" means to listen on all IPv4 addresses. + *

+ * o "::" means to listen on all IPv6 addresses. + *

+ * o "localhost" means to listen on all protocol families supported by + * the SSH implementation on loopback addresses only ([RFC3330] and + * [RFC3513]). + *

+ * o "127.0.0.1" and "::1" indicate listening on the loopback + * interfaces for IPv4 and IPv6, respectively. + *

+ */ + public static final class Forward { + + private final String address; + private int port; + + /** + * Creates this forward with address as {@code ""} and specified {@code port}. + * + * @param port + */ + public Forward(int port) { + this("", port); + } + + /** + * Creates this forward with specified {@code address} and port as {@code 0}. + * + * @param address + */ + public Forward(String address) { + this(address, 0); + } + + /** + * Creates this forward with specified {@code address} and {@code port} number. + * + * @param address address to bind + * @param port port number + */ + public Forward(String address, int port) { + this.address = address; + this.port = port; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) + return false; + Forward other = (Forward) obj; + return address.equals(other.address) && port == other.port; + } + + /** Returns the address represented by this forward. */ + public String getAddress() { + return address; + } + + /** Returns the port represented by this forward. */ + public int getPort() { + return port; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return address + ":" + port; + } + + } + + /** A {@code forwarded-tcpip} channel. */ + public static class ForwardedTCPIPChannel extends AbstractForwardedChannel { + + public static final String TYPE = "forwarded-tcpip"; + + private final Forward fwd; + + public ForwardedTCPIPChannel(Connection conn, int recipient, int remoteWinSize, int remoteMaxPacketSize, + Forward fwd, String origIP, int origPort) throws TransportException { + super(TYPE, conn, recipient, remoteWinSize, remoteMaxPacketSize, origIP, origPort); + this.fwd = fwd; + } + + /** Returns the forwarding from which this channel originates. */ + public Forward getParentForward() { + return fwd; + } + + } + + protected static final String PF_REQ = "tcpip-forward"; + protected static final String PF_CANCEL = "cancel-tcpip-forward"; + + protected final Map listeners = new ConcurrentHashMap(); + + public RemotePortForwarder(Connection conn) { + super(ForwardedTCPIPChannel.TYPE, conn); + } + + /** + * Request forwarding from the remote host on the specified {@link Forward}. Forwarded connections will be handled + * by supplied {@code listener}. + *

+ * If {@code forward} specifies as 0, the returned forward will have the correct port number as informed by remote + * host. + * + * @param forward the {@link Forward} to put in place on remote host + * @param listener the listener which will next forwarded connection + * + * @return the {@link Forward} which was put into place on the remote host + * + * @throws net.schmizz.sshj.connection.ConnectionException + * if there is an error requesting the forwarding + */ + public Forward bind(Forward forward, ConnectListener listener) throws ConnectionException, TransportException { + SSHPacket reply = req(PF_REQ, forward); + if (forward.port == 0) + forward.port = reply.readInt(); + log.info("Remote end listening on {}", forward); + listeners.put(forward, listener); + return forward; + } + + /** + * Request cancellation of some forwarding. + * + * @param forward the forward which is being cancelled + * + * @throws ConnectionException if there is an error with the cancellation request + */ + public void cancel(Forward forward) throws ConnectionException, TransportException { + try { + req(PF_CANCEL, forward); + } finally { + listeners.remove(forward); + } + } + + protected SSHPacket req(String reqName, Forward forward) throws ConnectionException, TransportException { + return conn.sendGlobalRequest(PF_REQ, true, new Buffer.PlainBuffer() // + .putString(forward.address) // + .putInt(forward.port)) // + .get(conn.getTimeout(), TimeUnit.SECONDS); + } + + /** Returns the active forwards. */ + public Set getActiveForwards() { + return listeners.keySet(); + } + + /** + * Internal API. Creates a {@link ForwardedTCPIPChannel} from the {@code CHANNEL_OPEN} request and calls associated + * {@code ConnectListener} for that forward in a separate thread. + */ + public void handleOpen(SSHPacket buf) throws ConnectionException, TransportException { + ForwardedTCPIPChannel chan = new ForwardedTCPIPChannel(conn, buf.readInt(), buf.readInt(), buf.readInt(), // + new Forward(buf.readString(), buf.readInt()), // + buf.readString(), buf.readInt()); + if (listeners.containsKey(chan.getParentForward())) + callListener(listeners.get(chan.getParentForward()), chan); + else + chan.reject(OpenFailException.Reason.ADMINISTRATIVELY_PROHIBITED, "Forwarding was not requested on `" + + chan.getParentForward() + "`"); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/SocketForwardingConnectListener.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/SocketForwardingConnectListener.java new file mode 100644 index 00000000..d3ec0ef3 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/SocketForwardingConnectListener.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.common.StreamCopier.ErrorCallback; +import net.schmizz.sshj.connection.channel.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; + +/** + * A {@link net.schmizz.sshj.connection.channel.forwarded.ConnectListener} that forwards what is received over the + * channel to a socket and vice-versa. + */ +public class SocketForwardingConnectListener implements ConnectListener { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final SocketAddress addr; + + /** Create with a {@link SocketAddress} this listener will forward to. */ + public SocketForwardingConnectListener(SocketAddress addr) { + this.addr = addr; + } + + /** On connect, confirm the channel and start forwarding. */ + public void gotConnect(Channel.Forwarded chan) throws IOException { + log.info("New connection from " + chan.getOriginatorIP() + ":" + chan.getOriginatorPort()); + + final Socket sock = new Socket(); + sock.setSendBufferSize(chan.getLocalMaxPacketSize()); + sock.setReceiveBufferSize(chan.getRemoteMaxPacketSize()); + + sock.connect(addr); + + // ok so far -- could connect, let's confirm the channel + chan.confirm(); + + final ErrorCallback closer = StreamCopier.closeOnErrorCallback(chan, new Closeable() { + public void close() throws IOException { + sock.close(); + } + }); + + new StreamCopier("soc2chan", sock.getInputStream(), chan.getOutputStream()) + .bufSize(chan.getRemoteMaxPacketSize()) + .errorCallback(closer) + .daemon(true) + .start(); + + new StreamCopier("chan2soc", chan.getInputStream(), sock.getOutputStream()) + .bufSize(chan.getLocalMaxPacketSize()) + .errorCallback(closer) + .daemon(true) + .start(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/connection/channel/forwarded/X11Forwarder.java b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/X11Forwarder.java new file mode 100644 index 00000000..19e4dbfb --- /dev/null +++ b/src/main/java/net/schmizz/sshj/connection/channel/forwarded/X11Forwarder.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.connection.channel.forwarded; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.connection.Connection; +import net.schmizz.sshj.connection.ConnectionException; +import net.schmizz.sshj.transport.TransportException; + +/** + * Handles forwarded {@code x11} channels. The actual request to forward X11 should be made from the specific {@link + * net.schmizz.sshj.connection.channel.direct.Session}. + */ +public class X11Forwarder extends AbstractForwardedChannelOpener { + + /** An {@code x11} forwarded channel. */ + public static class X11Channel extends AbstractForwardedChannel { + + public static final String TYPE = "x11"; + + public X11Channel(Connection conn, int recipient, int remoteWinSize, int remoteMaxPacketSize, String origIP, + int origPort) { + super(TYPE, conn, recipient, remoteWinSize, remoteMaxPacketSize, origIP, origPort); + } + + } + + private final ConnectListener listener; + + /** + * Creates and registers itself with {@code conn}. + * + * @param conn connection layer + * @param listener listener which will be delegated {@link X11Channel}'s to next + */ + public X11Forwarder(Connection conn, ConnectListener listener) { + super(X11Channel.TYPE, conn); + this.listener = listener; + } + + /** Internal API */ + public void handleOpen(SSHPacket buf) throws ConnectionException, TransportException { + callListener(listener, new X11Channel(conn, buf.readInt(), buf.readInt(), buf.readInt(), buf.readString(), buf + .readInt())); + } + + /** Stop handling {@code x11} channel open requests. De-registers itself with connection layer. */ + public void stop() { + conn.forget(this); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/sftp/FileAttributes.java b/src/main/java/net/schmizz/sshj/sftp/FileAttributes.java new file mode 100644 index 00000000..3211ba63 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/FileAttributes.java @@ -0,0 +1,229 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.xfer.FilePermission; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public final class FileAttributes { + + public static enum Flag { + + SIZE(0x00000001), UIDGID(0x00000002), MODE(0x00000004), ACMODTIME(0x00000008), EXTENDED(0x80000000); + + private final int flag; + + private Flag(int flag) { + this.flag = flag; + } + + public boolean isSet(int mask) { + return (mask & flag) == flag; + } + + public int get() { + return flag; + } + + } + + private final FileMode mode; + private final int mask; + private final long size; + private final int uid; + private final int gid; + private final long atime; + private final long mtime; + private final Map ext = new HashMap(); + + public FileAttributes() { + size = atime = mtime = uid = gid = mask = 0; + mode = new FileMode(0); + } + + public FileAttributes(int mask, long size, int uid, int gid, FileMode mode, long atime, long mtime, + Map ext) { + this.mask = mask; + this.size = size; + this.uid = uid; + this.gid = gid; + this.mode = mode; + this.atime = atime; + this.mtime = mtime; + this.ext.putAll(ext); + } + + public boolean has(Flag flag) { + return flag.isSet(mask); + } + + public long getSize() { + return size; + } + + public int getUID() { + return uid; + } + + public int getGID() { + return gid; + } + + public FileMode getMode() { + return mode; + } + + public Set getPermissions() { + return mode.getPermissions(); + } + + public FileMode.Type getType() { + return mode.getType(); + } + + public long getAtime() { + return atime; + } + + public long getMtime() { + return mtime; + } + + public String getExtended(String type) { + return ext.get(type); + } + + public byte[] toBytes() { + Buffer.PlainBuffer buf = new Buffer.PlainBuffer(); + buf.putInt(mask); + + if (has(Flag.SIZE)) + buf.putUINT64(size); + + if (has(Flag.UIDGID)) { + buf.putInt(uid); + buf.putInt(gid); + } + + if (has(Flag.MODE)) + buf.putInt(mode.getMask()); + + if (has(Flag.ACMODTIME)) { + buf.putInt(atime); + buf.putInt(mtime); + } + + if (has(Flag.EXTENDED)) { + buf.putInt(ext.size()); + for (Entry entry : ext.entrySet()) { + buf.putString(entry.getKey()); + buf.putString(entry.getValue()); + } + } + return buf.getCompactData(); + } + + public static class Builder { + + private int mask; + private long size; + private long atime; + private long mtime; + private FileMode mode = new FileMode(0); + private int uid; + private int gid; + private final Map ext = new HashMap(); + + public Builder withSize(long size) { + mask |= Flag.SIZE.get(); + this.size = size; + return this; + } + + public Builder withAtimeMtime(long atime, long mtime) { + mask |= Flag.ACMODTIME.get(); + this.atime = atime; + this.mtime = mtime; + return this; + } + + public Builder withUIDGID(int uid, int gid) { + mask |= Flag.UIDGID.get(); + this.uid = uid; + this.gid = gid; + return this; + } + + public Builder withPermissions(Set perms) { + mask |= Flag.MODE.get(); + this.mode = new FileMode(FilePermission.toMask(perms)); + return this; + } + + public Builder withPermissions(int perms) { + mask |= Flag.MODE.get(); + this.mode = new FileMode(perms); + return this; + } + + public Builder withExtended(String type, String data) { + mask |= Flag.EXTENDED.get(); + ext.put(type, data); + return this; + } + + public Builder withExtended(Map ext) { + mask |= Flag.EXTENDED.get(); + this.ext.putAll(ext); + return this; + } + + public FileAttributes build() { + return new FileAttributes(mask, size, uid, gid, mode, atime, mtime, ext); + } + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("["); + + if (has(Flag.SIZE)) + sb.append("size=").append(size).append(";"); + + if (has(Flag.UIDGID)) + sb.append("uid=").append(size).append(",gid=").append(gid).append(";"); + + if (has(Flag.MODE)) + sb.append("mode=").append(mode.toString()).append(";"); + + if (has(Flag.ACMODTIME)) + sb.append("atime=").append(atime).append(",mtime=").append(mtime).append(";"); + + if (has(Flag.EXTENDED)) + sb.append("ext=").append(ext); + + sb.append("]"); + + return sb.toString(); + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/FileMode.java b/src/main/java/net/schmizz/sshj/sftp/FileMode.java new file mode 100644 index 00000000..eee1db9e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/FileMode.java @@ -0,0 +1,97 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.xfer.FilePermission; + +import java.util.Collections; +import java.util.Set; + +public class FileMode { + + public static enum Type { + /** block special */ + BLOCK_SPECIAL(0060000), + /** character special */ + CHAR_SPECIAL(0020000), + /** FIFO special */ + FIFO_SPECIAL(0010000), + /** socket special */ + SOCKET_SPECIAL(0140000), + /** regular */ + REGULAR(0100000), + /** directory */ + DIRECTORY(0040000), + /** symbolic link */ + SYMKLINK(0120000), + /** unknown */ + UNKNOWN(0); + + private final int val; + + private Type(int val) { + this.val = val; + } + + public static Type fromMask(int mask) { + for (Type t : Type.values()) + if (t.val == mask) + return t; + return UNKNOWN; + } + + public static int toMask(Type t) { + return t.val; + } + + } + + private final int mask; + private final Type type; + private final Set perms; + + public FileMode(int mask) { + this.mask = mask; + this.type = Type.fromMask(getTypeMask()); + this.perms = FilePermission.fromMask(getPermissionsMask()); + } + + public int getMask() { + return mask; + } + + public int getTypeMask() { + return mask & 0770000; + } + + public int getPermissionsMask() { + return mask & 07777; + } + + public Type getType() { + return type; + } + + public Set getPermissions() { + return Collections.unmodifiableSet(perms); + } + + @Override + public String toString() { + return "[mask=" + Integer.toOctalString(mask) + "]"; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/OpenMode.java b/src/main/java/net/schmizz/sshj/sftp/OpenMode.java new file mode 100644 index 00000000..cf1cccdf --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/OpenMode.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import java.util.Set; + +public enum OpenMode { + + /** Open the file for reading. */ + READ(0x00000001), + /** + * Open the file for writing. If both this and {@link OpenMode#READ} are specified, the file is opened for both + * reading and writing. + */ + WRITE(0x00000002), + /** Force all writes to append data at the end of the file. */ + APPEND(0x00000004), + /** + * If this flag is specified, then a new file will be created if one does not already exist (if {@link + * OpenMode#TRUNC} is specified, the new file will be truncated to zero length if it previously exists). + */ + CREAT(0x00000008), + /** + * Forces an existing file with the same name to be truncated to zero length when creating a file by specifying + * {@link OpenMode#CREAT}. {@link OpenMode#CREAT} MUST also be specified if this flag is used. + */ + TRUNC(0x00000010), + /** + * Causes the request to fail if the named file already exists. {@link OpenMode#CREAT} MUST also be specified if + * this flag is used. + */ + EXCL(0x00000020); + + private final int pflag; + + private OpenMode(int pflag) { + this.pflag = pflag; + } + + public static int toMask(Set modes) { + int mask = 0; + for (OpenMode m : modes) + mask |= m.pflag; + return mask; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/PacketReader.java b/src/main/java/net/schmizz/sshj/sftp/PacketReader.java new file mode 100644 index 00000000..056d403d --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/PacketReader.java @@ -0,0 +1,103 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PacketReader extends Thread { + + /** Logger */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final InputStream in; + private final Map> futures = new ConcurrentHashMap>(); + private final SFTPPacket packet = new SFTPPacket(); + private final byte[] lenBuf = new byte[4]; + + public PacketReader(InputStream in) { + this.in = in; + setName("sftp reader"); + } + + private void readIntoBuffer(byte[] buf, int off, int len) throws IOException { + int count = 0; + int read = 0; + while (count < len && ((read = in.read(buf, off + count, len - count)) != -1)) + count += read; + if (read == -1) + throw new SFTPException("EOF while reading packet"); + } + + private int getPacketLength() throws IOException { + readIntoBuffer(lenBuf, 0, lenBuf.length); + + return (int) (lenBuf[0] << 24 & 0xff000000L + | lenBuf[1] << 16 & 0x00ff0000L + | lenBuf[2] << 8 & 0x0000ff00L + | lenBuf[3] & 0x000000ffL + ); + } + + public SFTPPacket readPacket() throws IOException { + int len = getPacketLength(); + + packet.rpos(0); + packet.wpos(0); + + packet.ensureCapacity(len); + readIntoBuffer(packet.array(), 0, len); + + packet.wpos(len); + + return packet; + } + + @Override + public void run() { + try { + while (true) { + readPacket(); + handle(); + } + } catch (IOException e) { + for (Future future : futures.values()) + future.error(e); + } + } + + public void handle() throws SFTPException { + Response resp = new Response(packet); + Future future = futures.remove(resp.getRequestID()); + log.debug("Received {} packet", resp.getType()); + if (future == null) + throw new SFTPException("Received [" + resp.readType() + "] response for request-id " + resp.getRequestID() + + ", no such request was made"); + else + future.set(resp); + } + + public void expectResponseTo(Request req) { + futures.put(req.getRequestID(), req.getResponseFuture()); + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/PacketType.java b/src/main/java/net/schmizz/sshj/sftp/PacketType.java new file mode 100644 index 00000000..98a404a6 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/PacketType.java @@ -0,0 +1,71 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +public enum PacketType { + + UNKNOWN(0), + INIT(1), + VERSION(2), + OPEN(3), + CLOSE(4), + READ(5), + WRITE(6), + LSTAT(7), + FSTAT(8), + SETSTAT(9), + FSETSTAT(10), + OPENDIR(11), + READDIR(12), + REMOVE(13), + MKDIR(14), + RMDIR(15), + REALPATH(16), + STAT(17), + RENAME(18), + READLINK(19), + SYMLINK(20), + STATUS(101), + HANDLE(102), + DATA(103), + NAME(104), + ATTRS(105), + EXTENDED(200), + EXTENDED_REPLY(201); + + private final byte b; + + private static final PacketType[] cache = new PacketType[256]; + + static { + for (PacketType t : PacketType.values()) + if (cache[t.toByte() & 0xff] == null) + cache[t.toByte() & 0xff] = t; + } + + private PacketType(int b) { + this.b = (byte) b; + } + + public static PacketType fromByte(byte b) { + return cache[b & 0xff]; + } + + public byte toByte() { + return b; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/PathComponents.java b/src/main/java/net/schmizz/sshj/sftp/PathComponents.java new file mode 100644 index 00000000..0e72f019 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/PathComponents.java @@ -0,0 +1,71 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +class PathComponents { + + public static String adjustForParent(String parent, String path) { + return (path.startsWith("/")) ? path // Absolute path, nothing to adjust + : (parent + (parent.endsWith("/") ? "" : "/") + path); // Relative path + } + + private static String trimFinalSlash(String path) { + return path.endsWith("/") ? path.substring(0, path.length() - 1) : path; + } + + private final String parent; + private final String name; + private final String path; + + public PathComponents(String parent, String name) { + this.parent = parent; + this.name = name; + this.path = adjustForParent(parent, name); + } + + public String getParent() { + return parent; + } + + public String getName() { + return name; + } + + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (o instanceof PathComponents) { + final PathComponents that = (PathComponents) o; + return (trimFinalSlash(path).equals(trimFinalSlash(that.path))); + } + + return false; + } + + @Override + public int hashCode() { + return trimFinalSlash(path).hashCode(); + } + + @Override + public String toString() { + return "[parent=" + parent + "; name=" + name + "; path=" + path + "]"; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/sftp/PathHelper.java b/src/main/java/net/schmizz/sshj/sftp/PathHelper.java new file mode 100644 index 00000000..2636bd18 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/PathHelper.java @@ -0,0 +1,59 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import java.io.IOException; + +class PathHelper { + + private final SFTPEngine sftp; + private String dotDir; + + public PathHelper(SFTPEngine sftp) { + this.sftp = sftp; + } + + public PathComponents getComponents(String path) throws IOException { + if (path.isEmpty() || path.equals(".")) + return getComponents(getDotDir()); + + final int lastSlash = path.lastIndexOf("/"); + + if (lastSlash == -1) + if (path.equals("..")) + return getComponents(canon(path)); + else + return new PathComponents(getDotDir(), path); + + final String name = path.substring(lastSlash + 1); + + if (name.equals(".") || name.equals("..")) + return getComponents(canon(path)); + else { + final String parent = path.substring(0, lastSlash); + return new PathComponents(parent, name); + } + } + + private String getDotDir() throws IOException { + return (dotDir != null) ? dotDir : (dotDir = canon(".")); + } + + private String canon(String path) throws IOException { + return sftp.canonicalize(path); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java b/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java new file mode 100644 index 00000000..a3331e40 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/RandomAccessRemoteFile.java @@ -0,0 +1,272 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; + +public class RandomAccessRemoteFile implements DataInput, DataOutput { + + + private final byte[] singleByte = new byte[1]; + + private final RemoteFile rf; + + private long fp; + + public RandomAccessRemoteFile(RemoteFile rf) { + this.rf = rf; + } + + public long getFilePointer() { + return fp; + } + + public void seek(long fp) { + this.fp = fp; + } + + public int read() throws IOException { + return read(singleByte, 0, 1) == -1 ? -1 : singleByte[0]; + } + + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + final int count = rf.read(fp, b, off, len); + fp += count; + return count; + } + + public boolean readBoolean() throws IOException { + final int ch = read(); + if (ch < 0) + throw new EOFException(); + return (ch != 0); + } + + public byte readByte() throws IOException { + final int ch = this.read(); + if (ch < 0) + throw new EOFException(); + return (byte) (ch); + } + + public char readChar() throws IOException { + final int ch1 = this.read(); + final int ch2 = this.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (char) ((ch1 << 8) + ch2); + } + + public double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + public float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + public void readFully(byte[] b) throws IOException { + readFully(b, 0, b.length); + } + + public void readFully(byte[] b, int off, int len) throws IOException { + int n = 0; + do { + int count = read(b, off + n, len - n); + if (count < 0) + throw new EOFException(); + n += count; + } while (n < len); + } + + public int readInt() throws IOException { + final int ch1 = read(); + final int ch2 = read(); + final int ch3 = read(); + final int ch4 = read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) + throw new EOFException(); + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4); + } + + public String readLine() throws IOException { + StringBuffer input = new StringBuffer(); + int c = -1; + boolean eol = false; + + while (!eol) + switch (c = read()) { + case -1: + case '\n': + eol = true; + break; + case '\r': + eol = true; + long cur = getFilePointer(); + if ((read()) != '\n') + seek(cur); + break; + default: + input.append((char) c); + break; + } + + if ((c == -1) && (input.length() == 0)) + return null; + return input.toString(); + } + + public long readLong() throws IOException { + return ((long) (readInt()) << 32) + (readInt() & 0xFFFFFFFFL); + } + + public short readShort() throws IOException { + final int ch1 = this.read(); + final int ch2 = this.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (short) ((ch1 << 8) + ch2); + } + + public String readUTF() throws IOException { + return DataInputStream.readUTF(this); + } + + public int readUnsignedByte() throws IOException { + final int ch = this.read(); + if (ch < 0) + throw new EOFException(); + return ch; + } + + public int readUnsignedShort() throws IOException { + final int ch1 = this.read(); + final int ch2 = this.read(); + if ((ch1 | ch2) < 0) + throw new EOFException(); + return (ch1 << 8) + ch2; + } + + public int skipBytes(int n) throws IOException { + if (n <= 0) + return 0; + final long pos = getFilePointer(); + final long len = rf.length(); + long newpos = pos + n; + if (newpos > len) + newpos = len; + seek(newpos); + + /* return the actual number of bytes skipped */ + return (int) (newpos - pos); + } + + public void write(int i) throws IOException { + singleByte[0] = (byte) i; + write(singleByte); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + rf.write(fp, b, off, len); + fp += (len - off); + } + + public void writeBoolean(boolean v) throws IOException { + write(v ? 1 : 0); + } + + public void writeByte(int v) throws IOException { + write(v); + } + + public void writeBytes(String s) throws IOException { + final byte[] b = s.getBytes(); + write(b, 0, b.length); + } + + public void writeChar(int v) throws IOException { + write((v >>> 8) & 0xFF); + write(v & 0xFF); + } + + public void writeChars(String s) throws IOException { + final int clen = s.length(); + final int blen = 2 * clen; + final byte[] b = new byte[blen]; + final char[] c = new char[clen]; + s.getChars(0, clen, c, 0); + for (int i = 0, j = 0; i < clen; i++) { + b[j++] = (byte) (c[i] >>> 8); + b[j++] = (byte) c[i]; + } + write(b, 0, blen); + } + + public void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + public void writeFloat(float v) throws IOException { + writeInt(Float.floatToIntBits(v)); + } + + public void writeInt(int v) throws IOException { + write((v >>> 24) & 0xFF); + write((v >>> 16) & 0xFF); + write((v >>> 8) & 0xFF); + write(v & 0xFF); + } + + public void writeLong(long v) throws IOException { + write((int) (v >>> 56) & 0xFF); + write((int) (v >>> 48) & 0xFF); + write((int) (v >>> 40) & 0xFF); + write((int) (v >>> 32) & 0xFF); + write((int) (v >>> 24) & 0xFF); + write((int) (v >>> 16) & 0xFF); + write((int) (v >>> 8) & 0xFF); + write((int) v & 0xFF); + } + + public void writeShort(int v) throws IOException { + write((v >>> 8) & 0xFF); + write(v & 0xFF); + } + + public void writeUTF(String str) throws IOException { + final DataOutputStream dos = new DataOutputStream(rf.new RemoteFileOutputStream(fp)); + try { + dos.writeUTF(str); + } finally { + dos.close(); + } + fp += dos.size(); + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteDirectory.java b/src/main/java/net/schmizz/sshj/sftp/RemoteDirectory.java new file mode 100644 index 00000000..04cc3742 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteDirectory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.sftp.Response.StatusCode; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +public class RemoteDirectory extends RemoteResource { + + public RemoteDirectory(Requester requester, String path, String handle) { + super(requester, path, handle); + } + + public List scan(RemoteResourceFilter filter) throws IOException { + List rri = new LinkedList(); + loop: + for (; ;) { + Response res = requester.doRequest(newRequest(PacketType.READDIR)); + switch (res.getType()) { + + case NAME: + final int count = res.readInt(); + for (int i = 0; i < count; i++) { + final String name = res.readString(); + res.readString(); // long name - IGNORED - shdve never been in the protocol + final FileAttributes attrs = res.readFileAttributes(); + RemoteResourceInfo inf = new RemoteResourceInfo(path, name, attrs); + if (!(name.equals(".") || name.equals("..")) && (filter == null || filter.accept(inf))) + rri.add(inf); + } + break loop; + + case STATUS: + res.ensureStatusIs(StatusCode.EOF); + break loop; + + default: + throw new SFTPException("Unexpected packet: " + res.getType()); + } + } + return rri; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java b/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java new file mode 100644 index 00000000..e798eb6c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java @@ -0,0 +1,175 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.sftp.Response.StatusCode; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class RemoteFile extends RemoteResource { + + public RemoteFile(Requester requester, String path, String handle) { + super(requester, path, handle); + } + + public RemoteFileInputStream getInputStream() { + return new RemoteFileInputStream(); + } + + public RemoteFileOutputStream getOutputStream() { + return new RemoteFileOutputStream(); + } + + public FileAttributes fetchAttributes() throws IOException { + return requester.doRequest(newRequest(PacketType.FSTAT)) // + .ensurePacketTypeIs(PacketType.ATTRS) // + .readFileAttributes(); + } + + public long length() throws IOException { + return fetchAttributes().getSize(); + } + + public void setLength(long len) throws IOException { + setAttributes(new FileAttributes.Builder().withSize(len).build()); + } + + public int read(long fileOffset, byte[] to, int offset, int len) throws IOException { + Response res = requester.doRequest(newRequest(PacketType.READ).putUINT64(fileOffset).putInt(len)); + switch (res.getType()) { + case DATA: + int recvLen = res.readInt(); + System.arraycopy(res.array(), res.rpos(), to, offset, recvLen); + return recvLen; + + case STATUS: + res.ensureStatusIs(StatusCode.EOF); + return -1; + + default: + throw new SFTPException("Unexpected packet: " + res.getType()); + } + } + + public void write(long fileOffset, byte[] data, int off, int len) throws IOException { + requester.doRequest( // + newRequest(PacketType.WRITE) // + .putUINT64(fileOffset) // + .putInt(len - off) // + .putRawBytes(data, off, len) // + ).ensureStatusPacketIsOK(); + } + + public void setAttributes(FileAttributes attrs) throws IOException { + requester.doRequest(newRequest(PacketType.FSETSTAT).putFileAttributes(attrs)).ensureStatusPacketIsOK(); + } + + public int getOutgoingPacketOverhead() { + return 1 + // packet type + 4 + // request id + 4 + // next length + handle.length() + // next + 8 + // file offset + 4 + // data length + 4; // packet length + } + + public class RemoteFileOutputStream extends OutputStream { + + + private final byte[] b = new byte[1]; + + private long fileOffset; + + public RemoteFileOutputStream() { + this(0); + } + + public RemoteFileOutputStream(long fileOffset) { + this.fileOffset = fileOffset; + } + + @Override + public void write(int w) throws IOException { + b[0] = (byte) w; + write(b, 0, 1); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + RemoteFile.this.write(fileOffset, buf, off, len); + fileOffset += len; + } + + } + + public class RemoteFileInputStream extends InputStream { + + private final byte[] b = new byte[1]; + + private long fileOffset; + private long markPos; + private long readLimit; + + public RemoteFileInputStream() { + this(0); + } + + public RemoteFileInputStream(int fileOffset) { + this.fileOffset = fileOffset; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readLimit) { + this.readLimit = readLimit; + markPos = fileOffset; + } + + @Override + public void reset() throws IOException { + fileOffset = markPos; + } + + @Override + public long skip(long n) throws IOException { + return (this.fileOffset = Math.min(fileOffset + n, length())); + } + + @Override + public int read() throws IOException { + return read(b, 0, 1) == -1 ? -1 : b[0]; + } + + @Override + public int read(byte[] into, int off, int len) throws IOException { + int read = RemoteFile.this.read(fileOffset, into, off, len); + if (read != -1) { + fileOffset += read; + if (markPos != 0 && read > readLimit) // Invalidate mark position + markPos = 0; + } + return read; + } + + } +} diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteResource.java b/src/main/java/net/schmizz/sshj/sftp/RemoteResource.java new file mode 100644 index 00000000..a878c77a --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteResource.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; + +abstract class RemoteResource implements Closeable { + + /** Logger */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final Requester requester; + protected final String path; + protected final String handle; + + protected RemoteResource(Requester requester, String path, String handle) { + this.requester = requester; + this.path = path; + this.handle = handle; + } + + public String getPath() { + return path; + } + + protected Request newRequest(PacketType type) { + return requester.newRequest(type).putString(handle); + } + + public void close() throws IOException { + log.info("Closing `{}`", this); + requester.doRequest(newRequest(PacketType.CLOSE)).ensureStatusPacketIsOK(); + } + + @Override + public String toString() { + return "RemoteResource{" + path + "}"; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteResourceFilter.java b/src/main/java/net/schmizz/sshj/sftp/RemoteResourceFilter.java new file mode 100644 index 00000000..2fee9469 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteResourceFilter.java @@ -0,0 +1,22 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +public interface RemoteResourceFilter { + + boolean accept(RemoteResourceInfo resource); + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteResourceInfo.java b/src/main/java/net/schmizz/sshj/sftp/RemoteResourceInfo.java new file mode 100644 index 00000000..1fddea69 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteResourceInfo.java @@ -0,0 +1,80 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +public class RemoteResourceInfo { + + private final PathComponents comps; + private final FileAttributes attrs; + + public RemoteResourceInfo(String parent, String name, FileAttributes attrs) { + this.comps = new PathComponents(parent, name); + this.attrs = attrs; + } + + public String getParent() { + return comps.getParent(); + } + + public String getPath() { + return comps.getPath(); + } + + public String getName() { + return comps.getName(); + } + + public FileAttributes getAttributes() { + return attrs; + } + + public boolean isType(FileMode.Type type) { + return attrs.getType() == type; + } + + public boolean isRegularFile() { + return isType(FileMode.Type.REGULAR); + } + + public boolean isDirectory() { + return isType(FileMode.Type.DIRECTORY); + } + + public boolean isSymlink() { + return isType(FileMode.Type.SYMKLINK); + } + + @Override + public boolean equals(Object o) { + if (o instanceof RemoteResourceInfo) { + final RemoteResourceInfo that = (RemoteResourceInfo) o; + if (comps.equals(that.comps)) + return true; + } + return false; + } + + @Override + public int hashCode() { + return comps.hashCode(); + } + + @Override + public String toString() { + return "[" + attrs.getType() + "] " + getPath(); + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/Request.java b/src/main/java/net/schmizz/sshj/sftp/Request.java new file mode 100644 index 00000000..e75f8042 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/Request.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.concurrent.Future; + +public class Request extends SFTPPacket { + + private final PacketType type; + private final long reqID; + private final Future responseFuture; + + public Request(PacketType type, long reqID) { + super(); + this.reqID = reqID; + this.type = type; + responseFuture = new Future("sftp / " + reqID, SFTPException.chainer); + putByte(type.toByte()); + putInt(reqID); + } + + public long getRequestID() { + return reqID; + } + + public PacketType getType() { + return type; + } + + public Future getResponseFuture() { + return responseFuture; + } + + @Override + public String toString() { + return "Request{" + reqID + ";" + type + "}"; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/Requester.java b/src/main/java/net/schmizz/sshj/sftp/Requester.java new file mode 100644 index 00000000..d21ae05f --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/Requester.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import java.io.IOException; + +public interface Requester { + + Request newRequest(PacketType type); + + Response doRequest(Request req) throws IOException; + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/Response.java b/src/main/java/net/schmizz/sshj/sftp/Response.java new file mode 100644 index 00000000..d0086099 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/Response.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.common.Buffer; + +public class Response extends SFTPPacket { + + public static enum StatusCode { + UNKNOWN(-1), + OK(0), + EOF(1), + NO_SUCH_FILE(2), + PERMISSION_DENIED(3), + FAILURE(4), + BAD_MESSAGE(5), + NO_CONNECTION(6), + CONNECITON_LOST(7), + OP_UNSUPPORTED(8); + + private final int code; + + public static StatusCode fromInt(int code) { + for (StatusCode s : StatusCode.values()) + if (s.code == code) + return s; + return UNKNOWN; + } + + private StatusCode(int code) { + this.code = code; + } + + } + + private final PacketType type; + private final long reqID; + + public Response(Buffer pk) { + super(pk); + this.type = readType(); + this.reqID = readLong(); + } + + public long getRequestID() { + return reqID; + } + + public PacketType getType() { + return type; + } + + public StatusCode readStatusCode() { + return StatusCode.fromInt(readInt()); + } + + public Response ensurePacketTypeIs(PacketType pt) throws SFTPException { + if (getType() != pt) + if (getType() == PacketType.STATUS) + throw new SFTPException(readStatusCode(), readString()); + else + throw new SFTPException("Unexpected packet " + getType()); + return this; + } + + public Response ensureStatusPacketIsOK() throws SFTPException { + return ensurePacketTypeIs(PacketType.STATUS).ensureStatusIs(StatusCode.OK); + } + + public Response ensureStatusIs(StatusCode acceptable) throws SFTPException { + StatusCode sc = readStatusCode(); + if (sc != acceptable) + throw new SFTPException(sc, readString()); + return this; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java b/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java new file mode 100644 index 00000000..fb59063b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPClient.java @@ -0,0 +1,175 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import net.schmizz.sshj.xfer.FilePermission; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +public class SFTPClient { + + /** Logger */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private final SFTPEngine sftp; + private final SFTPFileTransfer xfer; + + public SFTPClient(SessionFactory ssh) throws IOException { + this.sftp = new SFTPEngine(ssh).init(); + this.xfer = new SFTPFileTransfer(sftp); + } + + public SFTPEngine getSFTPEngine() { + return sftp; + } + + public SFTPFileTransfer getFileTansfer() { + return xfer; + } + + public List ls(String path) throws IOException { + return ls(path, null); + } + + public List ls(String path, RemoteResourceFilter filter) throws IOException { + final RemoteDirectory dir = sftp.openDir(path); + try { + return dir.scan(filter); + } finally { + dir.close(); + } + } + + public RemoteFile open(String filename, Set mode, FileAttributes attrs) throws IOException { + log.debug("Opening `{}`", filename); + return sftp.open(filename, mode, attrs); + } + + public RemoteFile open(String filename, Set mode) throws IOException { + return open(filename, mode, new FileAttributes()); + } + + public RemoteFile open(String filename) throws IOException { + return open(filename, EnumSet.of(OpenMode.READ)); + } + + public void mkdir(String dirname) throws IOException { + sftp.makeDir(dirname); + } + + public void rename(String oldpath, String newpath) throws IOException { + sftp.rename(oldpath, newpath); + } + + public void rm(String filename) throws IOException { + sftp.remove(filename); + } + + public void rmdir(String dirname) throws IOException { + sftp.removeDir(dirname); + } + + public void symlink(String linkpath, String targetpath) throws IOException { + sftp.symlink(linkpath, targetpath); + } + + public int version() { + return sftp.getOperativeProtocolVersion(); + } + + public void setattr(String path, FileAttributes attrs) throws IOException { + sftp.setAttributes(path, attrs); + } + + public int uid(String path) throws IOException { + return stat(path).getUID(); + } + + public int gid(String path) throws IOException { + return stat(path).getGID(); + } + + public long atime(String path) throws IOException { + return stat(path).getAtime(); + } + + public long mtime(String path) throws IOException { + return stat(path).getMtime(); + } + + public Set perms(String path) throws IOException { + return stat(path).getPermissions(); + } + + public FileMode mode(String path) throws IOException { + return stat(path).getMode(); + } + + public FileMode.Type type(String path) throws IOException { + return stat(path).getType(); + } + + public String readlink(String path) throws IOException { + return sftp.readLink(path); + } + + public FileAttributes stat(String path) throws IOException { + return sftp.stat(path); + } + + public FileAttributes lstat(String path) throws IOException { + return sftp.lstat(path); + } + + public void chown(String path, int uid) throws IOException { + setattr(path, new FileAttributes.Builder().withUIDGID(uid, gid(path)).build()); + } + + public void chmod(String path, int perms) throws IOException { + setattr(path, new FileAttributes.Builder().withPermissions(perms).build()); + } + + public void chgrp(String path, int gid) throws IOException { + setattr(path, new FileAttributes.Builder().withUIDGID(uid(path), gid).build()); + } + + public void truncate(String path, long size) throws IOException { + setattr(path, new FileAttributes.Builder().withSize(size).build()); + } + + public String canonicalize(String path) throws IOException { + return sftp.canonicalize(path); + } + + public long size(String path) throws IOException { + return stat(path).getSize(); + } + + public void get(String source, String dest) throws IOException { + xfer.download(source, dest); + } + + public void put(String source, String dest) throws IOException { + xfer.upload(source, dest); + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java b/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java new file mode 100644 index 00000000..e974f3a0 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPEngine.java @@ -0,0 +1,215 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.connection.channel.direct.Session.Subsystem; +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class SFTPEngine implements Requester { + + /** Logger */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + public static final int PROTOCOL_VERSION = 3; + + public static final int DEFAULT_TIMEOUT = 30; + + private volatile int timeout = DEFAULT_TIMEOUT; + + private final Subsystem sub; + private final PacketReader reader; + private final OutputStream out; + + private long reqID; + private int negotiatedVersion; + private final Map serverExtensions = new HashMap(); + + public SFTPEngine(SessionFactory ssh) throws SSHException { + sub = ssh.startSession().startSubsystem("sftp"); + out = sub.getOutputStream(); + reader = new PacketReader(sub.getInputStream()); + } + + public Subsystem getSubsystem() { + return sub; + } + + public SFTPEngine init() throws IOException { + transmit(new SFTPPacket(PacketType.INIT).putInt(PROTOCOL_VERSION)); + + final SFTPPacket response = reader.readPacket(); + + final PacketType type = response.readType(); + if (type != PacketType.VERSION) + throw new SFTPException("Expected INIT packet, received: " + type); + + negotiatedVersion = response.readInt(); + log.info("Client version {}, server version {}", PROTOCOL_VERSION, negotiatedVersion); + if (negotiatedVersion < PROTOCOL_VERSION) + throw new SFTPException("Server reported protocol version: " + negotiatedVersion); + + while (response.available() > 0) + serverExtensions.put(response.readString(), response.readString()); + + // Start reader thread + reader.start(); + return this; + } + + public int getOperativeProtocolVersion() { + return negotiatedVersion; + } + + public synchronized Request newRequest(PacketType type) { + return new Request(type, reqID = reqID + 1 & 0xffffffffL); + } + + public Response doRequest(Request req) throws IOException { + reader.expectResponseTo(req); + log.debug("Sending {}", req); + transmit(req); + return req.getResponseFuture().get(timeout, TimeUnit.SECONDS); + } + + private synchronized void transmit(SFTPPacket payload) throws IOException { + final int len = payload.available(); + out.write((len >>> 24) & 0xff); + out.write((len >>> 16) & 0xff); + out.write((len >>> 8) & 0xff); + out.write(len & 0xff); + out.write(payload.array(), 0, len); + out.flush(); + } + + public RemoteFile open(String path, Set modes, FileAttributes fa) throws IOException { + + final String handle = doRequest( + newRequest(PacketType.OPEN).putString(path).putInt(OpenMode.toMask(modes)).putFileAttributes(fa) + ).ensurePacketTypeIs(PacketType.HANDLE).readString(); + return new RemoteFile(this, path, handle); + } + + public RemoteFile open(String filename, Set modes) throws IOException { + return open(filename, modes, new FileAttributes()); + } + + public RemoteFile open(String filename) throws IOException { + return open(filename, EnumSet.of(OpenMode.READ)); + } + + public RemoteDirectory openDir(String path) throws IOException { + final String handle = doRequest( + newRequest(PacketType.OPENDIR).putString(path) + ).ensurePacketTypeIs(PacketType.HANDLE).readString(); + return new RemoteDirectory(this, path, handle); + } + + public void setAttributes(String path, FileAttributes attrs) throws IOException { + doRequest( + newRequest(PacketType.SETSTAT).putString(path).putFileAttributes(attrs) + ).ensureStatusPacketIsOK(); + } + + public String readLink(String path) throws IOException { + return readSingleName( + doRequest( + newRequest(PacketType.READLINK).putString(path) + )); + } + + public void makeDir(String path, FileAttributes attrs) throws IOException { + doRequest( + newRequest(PacketType.MKDIR).putString(path).putFileAttributes(attrs) + ).ensureStatusPacketIsOK(); + } + + public void makeDir(String path) throws IOException { + makeDir(path, new FileAttributes()); + } + + public void symlink(String linkpath, String targetpath) throws IOException { + doRequest( + newRequest(PacketType.SYMLINK).putString(linkpath).putString(targetpath) + ).ensureStatusPacketIsOK(); + } + + public void remove(String filename) throws IOException { + doRequest( + newRequest(PacketType.REMOVE).putString(filename) + ).ensureStatusPacketIsOK(); + } + + public void removeDir(String path) throws IOException { + doRequest( + newRequest(PacketType.RMDIR).putString(path) + ).ensureStatusIs(Response.StatusCode.OK); + } + + private FileAttributes stat(PacketType pt, String path) throws IOException { + return doRequest(newRequest(pt).putString(path)) + .ensurePacketTypeIs(PacketType.ATTRS) + .readFileAttributes(); + } + + public FileAttributes stat(String path) throws IOException { + return stat(PacketType.STAT, path); + } + + public FileAttributes lstat(String path) throws IOException { + return stat(PacketType.LSTAT, path); + } + + public void rename(String oldPath, String newPath) throws IOException { + doRequest( + newRequest(PacketType.RENAME).putString(oldPath).putString(newPath) + ).ensureStatusPacketIsOK(); + } + + public String canonicalize(String path) throws IOException { + return readSingleName( + doRequest( + newRequest(PacketType.REALPATH).putString(path) + )); + } + + private static String readSingleName(Response res) throws IOException { + res.ensurePacketTypeIs(PacketType.NAME); + if (res.readInt() == 1) + return res.readString(); + else + throw new SFTPException("Unexpected data in " + res.getType() + " packet"); + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public int getTimeout() { + return timeout; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPException.java b/src/main/java/net/schmizz/sshj/sftp/SFTPException.java new file mode 100644 index 00000000..287e4e85 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.concurrent.ExceptionChainer; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.sftp.Response.StatusCode; + +public class SFTPException extends SSHException { + + public static final ExceptionChainer chainer = new ExceptionChainer() { + + public SFTPException chain(Throwable t) { + if (t instanceof SFTPException) + return (SFTPException) t; + else + return new SFTPException(t); + } + + }; + + public SFTPException() { + super(); + } + + public SFTPException(DisconnectReason code) { + super(code); + } + + public SFTPException(DisconnectReason code, String message) { + super(code, message); + } + + public SFTPException(DisconnectReason code, String message, Throwable cause) { + super(code, message, cause); + } + + public SFTPException(DisconnectReason code, Throwable cause) { + super(code, cause); + } + + public SFTPException(String message) { + super(message); + } + + public SFTPException(String message, Throwable cause) { + super(message, cause); + } + + public SFTPException(Throwable cause) { + super(cause); + } + + private StatusCode sc; + + public StatusCode getStatusCode() { + return (sc == null) ? StatusCode.UNKNOWN : sc; + + } + + public SFTPException(StatusCode sc, String msg) { + this(msg); + this.sc = sc; + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java b/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java new file mode 100644 index 00000000..2a7425cb --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPFileTransfer.java @@ -0,0 +1,251 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.common.StreamCopier; +import net.schmizz.sshj.sftp.Response.StatusCode; +import net.schmizz.sshj.xfer.AbstractFileTransfer; +import net.schmizz.sshj.xfer.FileTransfer; +import net.schmizz.sshj.xfer.FileTransferUtil; +import net.schmizz.sshj.xfer.ModeGetter; +import net.schmizz.sshj.xfer.ModeSetter; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.EnumSet; + +public class SFTPFileTransfer extends AbstractFileTransfer implements FileTransfer { + + private final SFTPEngine sftp; + private final PathHelper pathHelper; + + private volatile FileFilter uploadFilter = defaultLocalFilter; + private volatile RemoteResourceFilter downloadFilter = defaultRemoteFilter; + + private static final FileFilter defaultLocalFilter = new FileFilter() { + public boolean accept(File pathName) { + return true; + } + }; + + private static final RemoteResourceFilter defaultRemoteFilter = new RemoteResourceFilter() { + public boolean accept(RemoteResourceInfo resource) { + return true; + } + }; + + public SFTPFileTransfer(SFTPEngine sftp) { + this.sftp = sftp; + this.pathHelper = new PathHelper(sftp); + } + + public void upload(String source, String dest) throws IOException { + new Uploader(getModeGetter(), getUploadFilter()).upload(new File(source), dest); + } + + public void download(String source, String dest) throws IOException { + PathComponents src = pathHelper.getComponents(source); + new Downloader(getModeSetter(), getDownloadFilter()).download(new RemoteResourceInfo(src.getParent(), src + .getName(), sftp.stat(source)), new File(dest)); + } + + public void setUploadFilter(FileFilter uploadFilter) { + this.uploadFilter = (this.uploadFilter == null) ? defaultLocalFilter : uploadFilter; + } + + public void setDownloadFilter(RemoteResourceFilter downloadFilter) { + this.downloadFilter = (this.downloadFilter == null) ? defaultRemoteFilter : downloadFilter; + } + + public FileFilter getUploadFilter() { + return uploadFilter; + } + + public RemoteResourceFilter getDownloadFilter() { + return downloadFilter; + } + + private class Downloader { + + private final ModeSetter modeSetter; + private final RemoteResourceFilter filter; + + Downloader(ModeSetter modeSetter, RemoteResourceFilter filter) { + this.modeSetter = modeSetter; + this.filter = filter; + } + + private void setAttributes(RemoteResourceInfo remote, File local) throws IOException { + final FileAttributes attrs = remote.getAttributes(); + modeSetter.setPermissions(local, attrs.getMode().getPermissionsMask()); + if (modeSetter.preservesTimes() && attrs.has(FileAttributes.Flag.ACMODTIME)) { + modeSetter.setLastAccessedTime(local, attrs.getAtime()); + modeSetter.setLastModifiedTime(local, attrs.getMtime()); + } + } + + private void downloadFile(RemoteResourceInfo remote, File local) throws IOException { + local = FileTransferUtil.getTargetFile(local, remote.getName()); + setAttributes(remote, local); + RemoteFile rf = sftp.open(remote.getPath()); + try { + final FileOutputStream fos = new FileOutputStream(local); + try { + StreamCopier.copy(rf.getInputStream(), fos, sftp.getSubsystem() + .getLocalMaxPacketSize(), false); + } finally { + fos.close(); + } + } finally { + rf.close(); + } + } + + private void downloadDir(RemoteResourceInfo remote, File local) throws IOException { + local = FileTransferUtil.getTargetDirectory(local, remote.getName()); + setAttributes(remote, local); + final RemoteDirectory rd = sftp.openDir(remote.getPath()); + for (RemoteResourceInfo rri : rd.scan(filter)) + download(rri, new File(local.getPath(), rri.getName())); + rd.close(); + } + + void download(RemoteResourceInfo remote, File local) throws IOException { + log.info("Downloading [{}] to [{}]", remote, local); + if (remote.isDirectory()) + downloadDir(remote, local); + else if (remote.isRegularFile()) + downloadFile(remote, local); + else + throw new IOException(remote + " is not a regular file or directory"); + } + } + + private class Uploader { + + private final ModeGetter modeGetter; + private final FileFilter filter; + + Uploader(ModeGetter modeGetter, FileFilter filter) { + this.modeGetter = modeGetter; + this.filter = filter; + } + + public FileAttributes getAttributes(File local) throws IOException { + FileAttributes.Builder builder = new FileAttributes.Builder().withPermissions(modeGetter + .getPermissions(local)); + if (modeGetter.preservesTimes()) + builder.withAtimeMtime(modeGetter.getLastAccessTime(local), modeGetter.getLastModifiedTime(local)); + return builder.build(); + } + + // tread carefully + private void setAttributes(FileAttributes current, File local, String remote) throws IOException { + final FileAttributes attrs = getAttributes(local); + // TODO whoaaa.. simplify? + if (!(current != null + && current.getMode().getPermissionsMask() == attrs.getMode().getPermissionsMask() + && (!modeGetter.preservesTimes() || (attrs.getAtime() == current.getAtime() && attrs.getMtime() == current + .getMtime())))) + sftp.setAttributes(remote, attrs); + } + + private String prepareDir(File local, String remote) throws IOException { + FileAttributes attrs; + try { + attrs = sftp.stat(remote); + } catch (SFTPException e) { + if (e.getStatusCode() == StatusCode.NO_SUCH_FILE) { + log.debug("probeDir: {} does not exist, creating", remote); + sftp.makeDir(remote, getAttributes(local)); + return remote; + } else + throw e; + } + + if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) + if (pathHelper.getComponents(remote).getName().equals(local.getName())) { + log.debug("probeDir: {} already exists", remote); + setAttributes(attrs, local, remote); + return remote; + } else { + log.debug("probeDir: {} already exists, path adjusted for {}", remote, local.getName()); + return prepareDir(local, PathComponents.adjustForParent(remote, local.getName())); + } + else + throw new IOException(attrs.getMode().getType() + " file already exists at " + remote); + } + + private String prepareFile(File local, String remote) throws IOException { + FileAttributes attrs; + try { + attrs = sftp.stat(remote); + } catch (SFTPException e) { + if (e.getStatusCode() == StatusCode.NO_SUCH_FILE) { + log.debug("probeFile: {} does not exist", remote); + return remote; + } else + throw e; + } + if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) { + log.debug("probeFile: {} was directory, path adjusted for {}", remote, local.getName()); + remote = PathComponents.adjustForParent(remote, local.getName()); + return remote; + } else { + log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType()); + return remote; + } + } + + private void uploadDir(File local, String remote) throws IOException { + final String adjusted = prepareDir(local, remote); + for (File f : local.listFiles(filter)) + upload(f, adjusted); + } + + private void uploadFile(File local, String remote) throws IOException { + final String adjusted = prepareFile(local, remote); + final RemoteFile rf = sftp.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC), + getAttributes(local)); + try { + final FileInputStream fis = new FileInputStream(local); + try { + StreamCopier.copy(fis, // + rf.getOutputStream(), sftp.getSubsystem().getRemoteMaxPacketSize() + - rf.getOutgoingPacketOverhead(), false); + } finally { + fis.close(); + } + } finally { + rf.close(); + } + } + + void upload(File local, String remote) throws IOException { + log.info("Uploading [{}] to [{}]", local, remote); + if (local.isDirectory()) + uploadDir(local, remote); + else if (local.isFile()) + uploadFile(local, remote); + else + throw new IOException(local + " is not a file or directory"); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/SFTPPacket.java b/src/main/java/net/schmizz/sshj/sftp/SFTPPacket.java new file mode 100644 index 00000000..8966ad06 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/SFTPPacket.java @@ -0,0 +1,66 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.common.Buffer; + +public class SFTPPacket> extends Buffer { + + public SFTPPacket() { + super(); + } + + public SFTPPacket(Buffer buf) { + super(buf); + } + + public SFTPPacket(PacketType pt) { + super(); + putByte(pt.toByte()); + } + + public FileAttributes readFileAttributes() { + final FileAttributes.Builder builder = new FileAttributes.Builder(); + final int mask = readInt(); + if (FileAttributes.Flag.SIZE.isSet(mask)) + builder.withSize(readUINT64()); + if (FileAttributes.Flag.UIDGID.isSet(mask)) + builder.withUIDGID(readInt(), readInt()); + if (FileAttributes.Flag.MODE.isSet(mask)) + builder.withPermissions(readInt()); + if (FileAttributes.Flag.ACMODTIME.isSet(mask)) + builder.withAtimeMtime(readInt(), readInt()); + if (FileAttributes.Flag.EXTENDED.isSet(mask)) { + final int extCount = readInt(); + for (int i = 0; i < extCount; i++) + builder.withExtended(readString(), readString()); + } + return builder.build(); + } + + public PacketType readType() { + return PacketType.fromByte(readByte()); + } + + public T putFileAttributes(FileAttributes fa) { + return putRawBytes(fa.toBytes()); + } + + public T putType(PacketType type) { + return putByte(type.toByte()); + } + +} diff --git a/src/main/java/net/schmizz/sshj/sftp/StatefulSFTPClient.java b/src/main/java/net/schmizz/sshj/sftp/StatefulSFTPClient.java new file mode 100644 index 00000000..e00eb32f --- /dev/null +++ b/src/main/java/net/schmizz/sshj/sftp/StatefulSFTPClient.java @@ -0,0 +1,150 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.sftp; + +import net.schmizz.sshj.connection.channel.direct.SessionFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +public class StatefulSFTPClient extends SFTPClient { + + private String cwd; + + public StatefulSFTPClient(SessionFactory ssh) throws IOException { + super(ssh); + this.cwd = getSFTPEngine().canonicalize("."); + log.info("Start dir = " + cwd); + } + + private synchronized String cwdify(String path) { + return PathComponents.adjustForParent(cwd, path); + } + + public synchronized void cd(String dirname) { + cwd = cwdify(dirname); + log.info("CWD = " + cwd); + } + + public synchronized List ls() throws IOException { + return ls(cwd, null); + } + + public synchronized List ls(RemoteResourceFilter filter) throws IOException { + return ls(cwd, filter); + } + + public synchronized String pwd() throws IOException { + return super.canonicalize(cwd); + } + + @Override + public List ls(String path) throws IOException { + return ls(path, null); + } + + @Override + public List ls(String path, RemoteResourceFilter filter) throws IOException { + final RemoteDirectory dir = getSFTPEngine().openDir(cwdify(path)); + try { + return dir.scan(filter); + } finally { + dir.close(); + } + } + + @Override + public RemoteFile open(String filename, Set mode, FileAttributes attrs) throws IOException { + return super.open(cwdify(filename), mode, attrs); + } + + @Override + public RemoteFile open(String filename, Set mode) throws IOException { + return super.open(cwdify(filename), mode); + } + + @Override + public RemoteFile open(String filename) throws IOException { + return super.open(cwdify(filename)); + } + + @Override + public void mkdir(String dirname) throws IOException { + super.mkdir(cwdify(dirname)); + } + + @Override + public void rename(String oldpath, String newpath) throws IOException { + super.rename(cwdify(oldpath), cwdify(newpath)); + } + + @Override + public void rm(String filename) throws IOException { + super.rm(cwdify(filename)); + } + + @Override + public void rmdir(String dirname) throws IOException { + super.rmdir(cwdify(dirname)); + } + + @Override + public void symlink(String linkpath, String targetpath) throws IOException { + super.symlink(cwdify(linkpath), cwdify(targetpath)); + } + + @Override + public void setattr(String path, FileAttributes attrs) throws IOException { + super.setattr(cwdify(path), attrs); + } + + @Override + public String readlink(String path) throws IOException { + return super.readlink(cwdify(path)); + } + + @Override + public FileAttributes stat(String path) throws IOException { + return super.stat(cwdify(path)); + } + + @Override + public FileAttributes lstat(String path) throws IOException { + return super.lstat(cwdify(path)); + } + + @Override + public void truncate(String path, long size) throws IOException { + super.truncate(cwdify(path), size); + } + + @Override + public String canonicalize(String path) throws IOException { + return super.canonicalize(cwdify(path)); + } + + @Override + public void get(String source, String dest) throws IOException { + super.get(cwdify(source), dest); + } + + @Override + public void put(String source, String dest) throws IOException { + super.get(source, cwdify(dest)); + } + +} diff --git a/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java b/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java new file mode 100644 index 00000000..5d5106a1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java @@ -0,0 +1,100 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.signature; + +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; + +/** An abstract class for {@link Signature} that implements common functionality. */ +public abstract class AbstractSignature implements Signature { + + protected java.security.Signature signature; + protected String algorithm; + + protected AbstractSignature(String algorithm) { + this.algorithm = algorithm; + } + + public void init(PublicKey pubkey, PrivateKey prvkey) { + try { + signature = SecurityUtils.getSignature(algorithm); + if (pubkey != null) + signature.initVerify(pubkey); + if (prvkey != null) + signature.initSign(prvkey); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + } + + public void update(byte[] foo) { + update(foo, 0, foo.length); + } + + public void update(byte[] foo, int off, int len) { + try { + signature.update(foo, off, len); + } catch (SignatureException e) { + throw new SSHRuntimeException(e); + } + } + + protected byte[] extractSig(byte[] sig) { + if (sig[0] == 0 && sig[1] == 0 && sig[2] == 0) { + int i = 0; + int j; + j = sig[i++] << 24 & 0xff000000 // + | sig[i++] << 16 & 0x00ff0000 // + | sig[i++] << 8 & 0x0000ff00 // + | sig[i++] & 0x000000ff; + i += j; + j = sig[i++] << 24 & 0xff000000 // + | sig[i++] << 16 & 0x00ff0000 // + | sig[i++] << 8 & 0x0000ff00 // + | sig[i++] & 0x000000ff; + byte[] tmp = new byte[j]; + System.arraycopy(sig, i, tmp, 0, j); + sig = tmp; + } + return sig; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/signature/Signature.java b/src/main/java/net/schmizz/sshj/signature/Signature.java new file mode 100644 index 00000000..84b4f7b6 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/signature/Signature.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.signature; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Signature interface for SSH used to sign or verify data. + *

+ * Usually wraps a javax.crypto.Signature object. + */ +public interface Signature { + + /** + * Initialize this signature with the given public key and private key. If the private key is null, only signature + * verification can be performed. + * + * @param pubkey (null-ok) specify in case verification is needed + * @param prvkey (null-ok) specify in case signing is needed + */ + void init(PublicKey pubkey, PrivateKey prvkey); + + /** + * Compute the signature + * + * @return the computed signature + */ + byte[] sign(); + + /** + * Convenience method for {@link #update(byte[], int, int)} + * + * @param H the byte-array to update with + */ + void update(byte[] H); + + /** + * Update the computed signature with the given data + * + * @param H byte-array to update with + * @param off offset within the array + * @param len length until which to compute + */ + void update(byte[] H, int off, int len); + + /** + * Verify against the given signature + * + * @param sig + * + * @return {@code true} on successful verification, {@code false} on failure + */ + boolean verify(byte[] sig); + +} diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java new file mode 100644 index 00000000..f572ee96 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java @@ -0,0 +1,125 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.signature; + +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SSHRuntimeException; + +import java.security.SignatureException; + +/** DSA {@link Signature} */ +public class SignatureDSA extends AbstractSignature { + + /** A named factory for DSA signature */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public Signature create() { + return new SignatureDSA(); + } + + public String getName() { + return KeyType.DSA.toString(); + } + + } + + public SignatureDSA() { + super("SHA1withDSA"); + } + + public byte[] sign() { + byte[] sig; + try { + sig = signature.sign(); + } catch (SignatureException e) { + throw new SSHRuntimeException(e); + } + + // sig is in ASN.1 + // SEQUENCE::={ r INTEGER, s INTEGER } + int len = 0; + int index = 3; + len = sig[index++] & 0xff; + byte[] r = new byte[len]; + System.arraycopy(sig, index, r, 0, r.length); + index = index + len + 1; + len = sig[index++] & 0xff; + byte[] s = new byte[len]; + System.arraycopy(sig, index, s, 0, s.length); + + byte[] result = new byte[40]; + + // result must be 40 bytes, but length of r and s may not be 20 bytes + + System.arraycopy(r, r.length > 20 ? 1 : 0, result, r.length > 20 ? 0 : 20 - r.length, r.length > 20 ? 20 + : r.length); + System.arraycopy(s, s.length > 20 ? 1 : 0, result, s.length > 20 ? 20 : 40 - s.length, s.length > 20 ? 20 + : s.length); + + return result; + } + + public boolean verify(byte[] sig) { + sig = extractSig(sig); + + // ASN.1 + int frst = (sig[0] & 0x80) != 0 ? 1 : 0; + int scnd = (sig[20] & 0x80) != 0 ? 1 : 0; + + int length = sig.length + 6 + frst + scnd; + byte[] tmp = new byte[length]; + tmp[0] = (byte) 0x30; + tmp[1] = (byte) 0x2c; + tmp[1] += frst; + tmp[1] += scnd; + tmp[2] = (byte) 0x02; + tmp[3] = (byte) 0x14; + tmp[3] += frst; + System.arraycopy(sig, 0, tmp, 4 + frst, 20); + tmp[4 + tmp[3]] = (byte) 0x02; + tmp[5 + tmp[3]] = (byte) 0x14; + tmp[5 + tmp[3]] += scnd; + System.arraycopy(sig, 20, tmp, 6 + tmp[3] + scnd, 20); + sig = tmp; + + try { + return signature.verify(sig); + } catch (SignatureException e) { + throw new SSHRuntimeException(e); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java new file mode 100644 index 00000000..51add884 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java @@ -0,0 +1,80 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.signature; + +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SSHRuntimeException; + +import java.security.SignatureException; + +/** RSA {@link Signature} */ +public class SignatureRSA extends AbstractSignature { + + /** A named factory for RSA {@link Signature} */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public Signature create() { + return new SignatureRSA(); + } + + public String getName() { + return KeyType.RSA.toString(); + } + + } + + public SignatureRSA() { + super("SHA1withRSA"); + } + + public byte[] sign() { + try { + return signature.sign(); + } catch (SignatureException e) { + throw new SSHRuntimeException(e); + } + } + + public boolean verify(byte[] sig) { + sig = extractSig(sig); + try { + return signature.verify(sig); + } catch (SignatureException e) { + throw new SSHRuntimeException(e); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/Converter.java b/src/main/java/net/schmizz/sshj/transport/Converter.java new file mode 100644 index 00000000..eb541670 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Converter.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.cipher.NoneCipher; +import net.schmizz.sshj.transport.compression.Compression; +import net.schmizz.sshj.transport.mac.MAC; + +/** + * Base class for {@link Encoder} and {@link Decoder}. + *

+ * From RFC 4253, p. 6 + *

+ *

+ *    Each packet is in the following format:
+ * 

+ * uint32 packet_length + * byte padding_length + * byte[n1] payload; n1 = packet_length - padding_length - 1 + * byte[n2] random padding; n2 = padding_length + * byte[m] mac (Message Authentication Code - MAC); m = mac_length + *

+ */ +abstract class Converter { + + protected Cipher cipher = new NoneCipher(); + protected MAC mac = null; + protected Compression compression = null; + + protected int cipherSize = 8; + protected long seq = -1; + protected boolean authed; + + long getSequenceNumber() { + return seq; + } + + void setAlgorithms(Cipher cipher, MAC mac, Compression compression) { + this.cipher = cipher; + this.mac = mac; + this.compression = compression; + if (compression != null) + compression.init(getCompressionType(), -1); + this.cipherSize = cipher.getIVSize(); + } + + void setAuthenticated() { + this.authed = true; + } + + boolean usingCompression() { + return compression != null && (authed || !compression.isDelayed()); + } + + abstract Compression.Type getCompressionType(); + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/Decoder.java b/src/main/java/net/schmizz/sshj/transport/Decoder.java new file mode 100644 index 00000000..6f970041 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Decoder.java @@ -0,0 +1,205 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.common.SSHPacketHandler; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.compression.Compression; +import net.schmizz.sshj.transport.mac.MAC; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Decodes packets from the SSH binary protocol per the current algorithms. */ +final class Decoder extends Converter { + + private static final int MAX_PACKET_LEN = 256 * 1024; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + /** What we pass decoded packets to */ + private final SSHPacketHandler packetHandler; + /** Buffer where as-yet undecoded data lives */ + private final SSHPacket inputBuffer = new SSHPacket(); + /** Used in case compression is active to store the uncompressed data */ + private final SSHPacket uncompressBuffer = new SSHPacket(); + /** MAC result is stored here */ + private byte[] macResult; + + /** -1 if packet length not yet been decoded, else the packet length */ + private int packetLength = -1; + + /** + * How many bytes do we need, before a call to decode() can succeed at decoding at least packet length, OR the whole + * packet? + */ + private int needed = 8; + + Decoder(SSHPacketHandler packetHandler) { + this.packetHandler = packetHandler; + } + + /** + * Returns advised number of bytes that should be made available in decoderBuffer before the method should be called + * again. + * + * @return number of bytes needed before further decoding possible + */ + private int decode() throws SSHException { + int need; + + /* Decoding loop */ + for (; ;) + + if (packetLength == -1) // Waiting for beginning of packet + { + + assert inputBuffer.rpos() == 0 : "buffer cleared"; + + need = cipherSize - inputBuffer.available(); + if (need <= 0) + packetLength = decryptLength(); + else + // Need more data + break; + + } else { + + assert inputBuffer.rpos() == 4 : "packet length read"; + + need = packetLength + (mac != null ? mac.getBlockSize() : 0) - inputBuffer.available(); + if (need <= 0) { + + decryptPayload(inputBuffer.array()); + + seq = seq + 1 & 0xffffffffL; + + if (mac != null) + checkMAC(inputBuffer.array()); + + // Exclude the padding & MAC + inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte()); + + final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer; + + if (log.isTraceEnabled()) + log.trace("Received packet #{}: {}", seq, plain.printHex()); + + packetHandler.handle(plain.readMessageID(), plain); // Process the decoded packet // + + inputBuffer.clear(); + packetLength = -1; + + } else + // Need more data + break; + } + + return need; + } + + private void checkMAC(final byte[] data) throws TransportException { + mac.update(seq); // seq num + mac.update(data, 0, packetLength + 4); // packetLength+4 = entire packet w/o mac + mac.doFinal(macResult, 0); // compute + // Check against the received MAC + if (!ByteArrayUtils.equals(macResult, 0, data, packetLength + 4, mac.getBlockSize())) + throw new TransportException(DisconnectReason.MAC_ERROR, "MAC Error"); + } + + private SSHPacket decompressed() throws TransportException { + uncompressBuffer.clear(); + compression.uncompress(inputBuffer, uncompressBuffer); + return uncompressBuffer; + } + + private int decryptLength() throws TransportException { + cipher.update(inputBuffer.array(), 0, cipherSize); + + final int len = inputBuffer.readInt(); // Read packet length + + if (isInvalidPacketLength(len)) { // Check packet length validity + log.info("Error decoding packet (invalid length) {}", inputBuffer.printHex()); + throw new TransportException(DisconnectReason.PROTOCOL_ERROR, "invalid packet length: " + len); + } + + return len; + } + + private static boolean isInvalidPacketLength(int len) { + return len < 5 || len > MAX_PACKET_LEN; + } + + private void decryptPayload(final byte[] data) { + cipher.update(data, cipherSize, packetLength + 4 - cipherSize); + } + + /** + * Adds {@code len} bytes from {@code b} to the decoder buffer. When a packet has been successfully decoded, hooks + * in to {@link net.schmizz.sshj.common.SSHPacketHandler#handle} of the {@link net.schmizz.sshj.common.SSHPacketHandler} + * this decoder was initialized with. + *

+ * Returns the number of bytes expected in the next call in order to decode the packet length, and if the packet + * length has already been decoded; to decode the payload. This number is accurate and should be taken to heart. + */ + int received(byte[] b, int len) throws SSHException { + inputBuffer.putRawBytes(b, 0, len); + if (needed <= len) + needed = decode(); + else + needed -= len; + return needed; + } + + @Override + void setAlgorithms(Cipher cipher, MAC mac, Compression compression) { + super.setAlgorithms(cipher, mac, compression); + macResult = new byte[mac.getBlockSize()]; + } + + @Override + Compression.Type getCompressionType() { + return Compression.Type.Inflater; + } + + int getMaxPacketLength() { + return MAX_PACKET_LEN; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/Encoder.java b/src/main/java/net/schmizz/sshj/transport/Encoder.java new file mode 100644 index 00000000..46879198 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Encoder.java @@ -0,0 +1,167 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.compression.Compression; +import net.schmizz.sshj.transport.mac.MAC; +import net.schmizz.sshj.transport.random.Random; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.locks.Lock; + +/** Encodes packets into the SSH binary protocol per the current algorithms. */ +final class Encoder extends Converter { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final Random prng; + + private final Lock encodeLock; + + Encoder(Random prng, Lock encodeLock) { + this.prng = prng; + this.encodeLock = encodeLock; + } + + private SSHPacket checkHeaderSpace(SSHPacket buffer) { + if (buffer.rpos() < 5) { + log.warn("Performance cost: when sending a packet, ensure that " + + "5 bytes are available in front of the buffer"); + SSHPacket nb = new SSHPacket(buffer.available() + 5); + nb.rpos(5); + nb.wpos(5); + nb.putBuffer(buffer); + buffer = nb; + } + return buffer; + } + + private void compress(SSHPacket buffer) { + compression.compress(buffer); + } + + private void putMAC(SSHPacket buffer, int startOfPacket, int endOfPadding) { + buffer.wpos(endOfPadding + mac.getBlockSize()); + mac.update(seq); + mac.update(buffer.array(), startOfPacket, endOfPadding); + mac.doFinal(buffer.array(), endOfPadding); + } + + /** + * Encode a buffer into the SSH binary protocol per the current algorithms. + * + * @param buffer the buffer to encode + * + * @return the sequence no. of encoded packet + * + * @throws TransportException + */ + long encode(SSHPacket buffer) { + encodeLock.lock(); + try { + buffer = checkHeaderSpace(buffer); + + if (log.isTraceEnabled()) + log.trace("Encoding packet #{}: {}", seq, buffer.printHex()); + + if (usingCompression()) + compress(buffer); + + final int payloadSize = buffer.available(); + + // Compute padding length + int padLen = -(payloadSize + 5) & cipherSize - 1; + if (padLen < cipherSize) + padLen += cipherSize; + + final int startOfPacket = buffer.rpos() - 5; + final int packetLen = payloadSize + 1 + padLen; + + // Put packet header + buffer.wpos(startOfPacket); + buffer.putInt(packetLen); + buffer.putByte((byte) padLen); + + // Now wpos will mark end of padding + buffer.wpos(startOfPacket + 5 + payloadSize + padLen); + // Fill padding + prng.fill(buffer.array(), buffer.wpos() - padLen, padLen); + + seq = seq + 1 & 0xffffffffL; + + if (mac != null) + putMAC(buffer, startOfPacket, buffer.wpos()); + + cipher.update(buffer.array(), startOfPacket, 4 + packetLen); + + buffer.rpos(startOfPacket); // Make ready-to-read + + return seq; + } finally { + encodeLock.unlock(); + } + } + + @Override + void setAlgorithms(Cipher cipher, MAC mac, Compression compression) { + encodeLock.lock(); + try { + super.setAlgorithms(cipher, mac, compression); + } finally { + encodeLock.unlock(); + } + } + + @Override + void setAuthenticated() { + encodeLock.lock(); + try { + super.setAuthenticated(); + } finally { + encodeLock.unlock(); + } + } + + @Override + Compression.Type getCompressionType() { + return Compression.Type.Deflater; + } + + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/Heartbeater.java b/src/main/java/net/schmizz/sshj/transport/Heartbeater.java new file mode 100644 index 00000000..0464615f --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Heartbeater.java @@ -0,0 +1,98 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class Heartbeater extends Thread { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final TransportProtocol trans; + + private int interval; + + private boolean started; + + Heartbeater(TransportProtocol trans) { + this.trans = trans; + setName("heartbeater"); + } + + synchronized void setInterval(int interval) { + this.interval = interval; + if (interval != 0) { + if (!started) + start(); + notify(); + } + } + + synchronized int getInterval() { + return interval; + } + + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + int hi; + synchronized (this) { + while ((hi = interval) == 0) + wait(); + } + if (!started) + started = true; + else if (trans.isRunning()) { + log.info("Sending heartbeat since {} seconds elapsed", hi); + trans.write(new SSHPacket(Message.IGNORE)); + } + Thread.sleep(hi * 1000); + } + } catch (Exception e) { + if (Thread.currentThread().isInterrupted()) { + // We are meant to shut up and draw to a close if interrupted + } else + trans.die(e); + } + + log.debug("Stopping"); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java b/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java new file mode 100644 index 00000000..9393a8a9 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java @@ -0,0 +1,365 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.concurrent.Event; +import net.schmizz.concurrent.FutureUtils; +import net.schmizz.sshj.common.*; +import net.schmizz.sshj.transport.cipher.Cipher; +import net.schmizz.sshj.transport.compression.Compression; +import net.schmizz.sshj.transport.digest.Digest; +import net.schmizz.sshj.transport.kex.KeyExchange; +import net.schmizz.sshj.transport.mac.MAC; +import net.schmizz.sshj.transport.verification.HostKeyVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.PublicKey; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** Algorithm negotiation and key exchange. */ +final class KeyExchanger implements SSHPacketHandler, ErrorNotifiable { + + private static enum Expected { + /** we have sent or are sending KEXINIT, and expect the server's KEXINIT */ + KEXINIT, + /** we are expecting some followup data as part of the exchange */ + FOLLOWUP, + /** we are expecting SSH_MSG_NEWKEYS */ + NEWKEYS, + } + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final TransportProtocol transport; + + /** + * {@link HostKeyVerifier#verify(String, int, java.security.PublicKey)} is invoked by {@link #verifyHost(PublicKey)} + * when we are ready to verify the the server's host key. + */ + private final Queue hostVerifiers = new LinkedList(); + + private final AtomicBoolean kexOngoing = new AtomicBoolean(); + + /** What we are expecting from the next packet */ + private Expected expected = Expected.KEXINIT; + + /** Instance of negotiated key exchange algorithm */ + private KeyExchange kex; + + /** Computed session ID */ + private byte[] sessionID; + + private Proposal clientProposal; + private NegotiatedAlgorithms negotiatedAlgs; + + private final Event kexInitSent = new Event("kexinit sent", + TransportException.chainer); + + private final Event done; + + KeyExchanger(TransportProtocol trans) { + this.transport = trans; + /* + * Use TransportProtocol's writeLock, since TransportProtocol.write() may wait on this event and the lock should + * be released while waiting. + */ + this.done = new Event("kex done", TransportException.chainer, trans.getWriteLock()); + } + + /** + * Add a callback for host key verification. + *

+ * Any of the {@link HostKeyVerifier} implementations added this way can deem a host key to be acceptable, allowing + * key exchange to successfuly complete. Otherwise, a {@link TransportException} will result during key exchange. + * + * @param hkv object whose {@link HostKeyVerifier#verify} method will be invoked + */ + synchronized void addHostKeyVerifier(HostKeyVerifier hkv) { + hostVerifiers.add(hkv); + } + + /** + * Returns the session identifier computed during key exchange. + * + * @return session identifier as a byte array + */ + byte[] getSessionID() { + return Arrays.copyOf(sessionID, sessionID.length); + } + + /** @return Whether key exchange has been completed */ + boolean isKexDone() { + return done.isSet(); + } + + /** @return Whether key exchange is currently ongoing */ + boolean isKexOngoing() { + return kexOngoing.get(); + } + + /** + * Starts key exchange by sending a {@code SSH_MSG_KEXINIT} packet. Key exchange needs to be done once mandatorily + * after initializing the {@link Transport} for it to be usable and may be initiated at any later point e.g. if + * {@link Transport#getConfig() algorithms} have changed and should be renegotiated. + * + * @param waitForDone whether should block till key exchange completed + * + * @throws TransportException if there is an error during key exchange + * @see {@link Transport#setTimeout} for setting timeout for kex + */ + void startKex(boolean waitForDone) throws TransportException { + if (!kexOngoing.getAndSet(true)) { + done.clear(); + sendKexInit(); + } + if (waitForDone) + waitForDone(); + } + + void waitForDone() throws TransportException { + done.await(transport.getTimeout(), TimeUnit.SECONDS); + } + + private synchronized void ensureKexOngoing() throws TransportException { + if (!isKexOngoing()) + throw new TransportException(DisconnectReason.PROTOCOL_ERROR, + "Key exchange packet received when key exchange was not ongoing"); + } + + private static void ensureReceivedMatchesExpected(Message got, Message expected) throws TransportException { + if (got != expected) + throw new TransportException(DisconnectReason.PROTOCOL_ERROR, "Was expecting " + expected); + } + + /** + * Sends SSH_MSG_KEXINIT and sets the {@link #kexInitSent} event. + * + * @throws TransportException + */ + private void sendKexInit() throws TransportException { + log.info("Sending SSH_MSG_KEXINIT"); + clientProposal = new Proposal(transport.getConfig()); + transport.write(clientProposal.getPacket()); + kexInitSent.set(); + } + + private void sendNewKeys() throws TransportException { + log.info("Sending SSH_MSG_NEWKEYS"); + transport.write(new SSHPacket(Message.NEWKEYS)); + } + + /** + * Tries to validate host key with all the host key verifiers known to this instance ( {@link #hostVerifiers}) + * + * @param key the host key to verify + * + * @throws TransportException + */ + private synchronized void verifyHost(PublicKey key) throws TransportException { + for (HostKeyVerifier hkv : hostVerifiers) { + log.debug("Trying to verify host key with {}", hkv); + if (hkv.verify(transport.getRemoteHost(), transport.getRemotePort(), key)) + return; + } + + throw new TransportException(DisconnectReason.HOST_KEY_NOT_VERIFIABLE, "Could not verify `" + + KeyType.fromKey(key) + "` host key with fingerprint `" + SecurityUtils.getFingerprint(key) + + "` for `" + transport.getRemoteHost() + "` on port " + transport.getRemotePort()); + } + + private void setKexDone() { + kexOngoing.set(false); + kexInitSent.clear(); + done.set(); + } + + private void gotKexInit(SSHPacket buf) throws TransportException { + Proposal serverProposal = new Proposal(buf); + negotiatedAlgs = clientProposal.negotiate(serverProposal); + log.debug("Negotiated algorithms: {}", negotiatedAlgs); + kex = Factory.Named.Util.create(transport.getConfig().getKeyExchangeFactories(), negotiatedAlgs + .getKeyExchangeAlgorithm()); + kex.init(transport, transport.getServerID().getBytes(), transport.getClientID().getBytes(), buf + .getCompactData(), clientProposal.getPacket().getCompactData()); + } + + /** + * Private method used while putting new keys into use that will resize the key used to initialize the cipher to the + * needed length. + * + * @param E the key to resize + * @param blockSize the cipher block size + * @param hash the hash algorithm + * @param K the key exchange K parameter + * @param H the key exchange H parameter + * + * @return the resized key + */ + private static byte[] resizedKey(byte[] E, int blockSize, Digest hash, byte[] K, byte[] H) { + while (blockSize > E.length) { + Buffer.PlainBuffer buffer = new Buffer.PlainBuffer().putMPInt(K).putRawBytes(H).putRawBytes(E); + hash.update(buffer.array(), 0, buffer.available()); + byte[] foo = hash.digest(); + byte[] bar = new byte[E.length + foo.length]; + System.arraycopy(E, 0, bar, 0, E.length); + System.arraycopy(foo, 0, bar, E.length, foo.length); + E = bar; + } + return E; + } + + /* See Sec. 7.2. "Output from Key Exchange", RFC 4253 */ + + private void gotNewKeys() { + final Digest hash = kex.getHash(); + + if (sessionID == null) + // session id is 'H' from the first key exchange and does not change thereafter + sessionID = Arrays.copyOf(kex.getH(), kex.getH().length); + + final Buffer.PlainBuffer hashInput = new Buffer.PlainBuffer() // + .putMPInt(kex.getK()) // + .putRawBytes(kex.getH()) // + .putByte((byte) 0) // + .putRawBytes(sessionID); + final int pos = hashInput.available() - sessionID.length - 1; // Position of + + hashInput.array()[pos] = 'A'; + hash.update(hashInput.array(), 0, hashInput.available()); + final byte[] initialIV_C2S = hash.digest(); + + hashInput.array()[pos] = 'B'; + hash.update(hashInput.array(), 0, hashInput.available()); + final byte[] initialIV_S2C = hash.digest(); + + hashInput.array()[pos] = 'C'; + hash.update(hashInput.array(), 0, hashInput.available()); + final byte[] encryptionKey_C2S = hash.digest(); + + hashInput.array()[pos] = 'D'; + hash.update(hashInput.array(), 0, hashInput.available()); + final byte[] encryptionKey_S2C = hash.digest(); + + hashInput.array()[pos] = 'E'; + hash.update(hashInput.array(), 0, hashInput.available()); + final byte[] integrityKey_C2S = hash.digest(); + + hashInput.array()[pos] = 'F'; + hash.update(hashInput.array(), 0, hashInput.available()); + final byte[] integrityKey_S2C = hash.digest(); + + final Cipher cipher_C2S = Factory.Named.Util.create(transport.getConfig().getCipherFactories(), negotiatedAlgs + .getClient2ServerCipherAlgorithm()); + cipher_C2S.init(Cipher.Mode.Encrypt, // + resizedKey(encryptionKey_C2S, cipher_C2S.getBlockSize(), hash, kex.getK(), kex.getH()), // + initialIV_C2S); + + final Cipher cipher_S2C = Factory.Named.Util.create(transport.getConfig().getCipherFactories(), // + negotiatedAlgs.getServer2ClientCipherAlgorithm()); + cipher_S2C.init(Cipher.Mode.Decrypt, // + resizedKey(encryptionKey_S2C, cipher_S2C.getBlockSize(), hash, kex.getK(), kex.getH()), // + initialIV_S2C); + + final MAC mac_C2S = Factory.Named.Util.create(transport.getConfig().getMACFactories(), negotiatedAlgs + .getClient2ServerMACAlgorithm()); + mac_C2S.init(integrityKey_C2S); + + final MAC mac_S2C = Factory.Named.Util.create(transport.getConfig().getMACFactories(), // + negotiatedAlgs.getServer2ClientMACAlgorithm()); + mac_S2C.init(integrityKey_S2C); + + final Compression compression_S2C = Factory.Named.Util.create(transport.getConfig().getCompressionFactories(), + negotiatedAlgs.getServer2ClientCompressionAlgorithm()); + final Compression compression_C2S = Factory.Named.Util.create(transport.getConfig().getCompressionFactories(), + negotiatedAlgs.getClient2ServerCompressionAlgorithm()); + + transport.getEncoder().setAlgorithms(cipher_C2S, mac_C2S, compression_C2S); + transport.getDecoder().setAlgorithms(cipher_S2C, mac_S2C, compression_S2C); + } + + public void handle(Message msg, SSHPacket buf) throws TransportException { + switch (expected) { + + case KEXINIT: + ensureReceivedMatchesExpected(msg, Message.KEXINIT); + log.info("Received SSH_MSG_KEXINIT"); + startKex(false); // Will start key exchange if not already on + /* + * We block on this event to prevent a race condition where we may have received a SSH_MSG_KEXINIT before + * having sent the packet ourselves (would cause gotKexInit() to fail) + */ + kexInitSent.await(transport.getTimeout(), TimeUnit.SECONDS); + buf.rpos(buf.rpos() - 1); + gotKexInit(buf); + expected = Expected.FOLLOWUP; + break; + + case FOLLOWUP: + ensureKexOngoing(); + log.info("Received kex followup data"); + if (kex.next(msg, buf)) { + verifyHost(kex.getHostKey()); + sendNewKeys(); + expected = Expected.NEWKEYS; + } + break; + + case NEWKEYS: + ensureReceivedMatchesExpected(msg, Message.NEWKEYS); + ensureKexOngoing(); + log.info("Received SSH_MSG_NEWKEYS"); + gotNewKeys(); + setKexDone(); + expected = Expected.KEXINIT; + break; + + default: + assert false; + + } + } + + public void notifyError(SSHException error) { + log.debug("Got notified of {}", error.toString()); + FutureUtils.alertAll(error, kexInitSent, done); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/NegotiatedAlgorithms.java b/src/main/java/net/schmizz/sshj/transport/NegotiatedAlgorithms.java new file mode 100644 index 00000000..7685c7e5 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/NegotiatedAlgorithms.java @@ -0,0 +1,107 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +public final class NegotiatedAlgorithms { + + private final String kex; + private final String sig; + private final String c2sCipher; + private final String s2cCipher; + private final String c2sMAC; + private final String s2cMAC; + private final String c2sComp; + private final String s2cComp; + + NegotiatedAlgorithms(String kex, String sig, String c2sCipher, String s2cCipher, String c2sMAC, String s2cMAC, + String c2sComp, String s2cComp) { + this.kex = kex; + this.sig = sig; + this.c2sCipher = c2sCipher; + this.s2cCipher = s2cCipher; + this.c2sMAC = c2sMAC; + this.s2cMAC = s2cMAC; + this.c2sComp = c2sComp; + this.s2cComp = s2cComp; + } + + public String getKeyExchangeAlgorithm() { + return kex; + } + + public String getSignatureAlgorithm() { + return sig; + } + + public String getClient2ServerCipherAlgorithm() { + return c2sCipher; + } + + public String getServer2ClientCipherAlgorithm() { + return s2cCipher; + } + + public String getClient2ServerMACAlgorithm() { + return c2sMAC; + } + + public String getServer2ClientMACAlgorithm() { + return s2cMAC; + } + + public String getClient2ServerCompressionAlgorithm() { + return c2sComp; + } + + public String getServer2ClientCompressionAlgorithm() { + return s2cComp; + } + + @Override + public String toString() { + return ("[ " + // + "kex=" + kex + "; " + // + "sig=" + sig + "; " + // + "c2sCipher=" + c2sCipher + "; " + // + "s2cCipher=" + s2cCipher + "; " + // + "c2sMAC=" + c2sMAC + "; " + // + "s2cMAC=" + s2cMAC + "; " + // + "c2sComp=" + c2sComp + "; " + // + "s2cComp=" + s2cComp + // + " ]"); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/Proposal.java b/src/main/java/net/schmizz/sshj/transport/Proposal.java new file mode 100644 index 00000000..aff3844b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Proposal.java @@ -0,0 +1,176 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.sshj.Config; +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; + +import java.util.Arrays; +import java.util.List; + +class Proposal { + + private final List kex; + private final List sig; + private final List c2sCipher; + private final List s2cCipher; + private final List c2sMAC; + private final List s2cMAC; + private final List c2sComp; + private final List s2cComp; + private final SSHPacket packet; + + public Proposal(Config config) { + kex = Factory.Named.Util.getNames(config.getKeyExchangeFactories()); + sig = Factory.Named.Util.getNames(config.getSignatureFactories()); + c2sCipher = s2cCipher = Factory.Named.Util.getNames(config.getCipherFactories()); + c2sMAC = s2cMAC = Factory.Named.Util.getNames(config.getMACFactories()); + c2sComp = s2cComp = Factory.Named.Util.getNames(config.getCompressionFactories()); + + packet = new SSHPacket(Message.KEXINIT); + + // Put cookie + packet.ensureCapacity(16); + config.getRandomFactory().create().fill(packet.array(), packet.wpos(), 16); + packet.wpos(packet.wpos() + 16); + + // Put algorithm lists + packet.putString(toCommaString(kex)); + packet.putString(toCommaString(sig)); + packet.putString(toCommaString(c2sCipher)); + packet.putString(toCommaString(s2cCipher)); + packet.putString(toCommaString(c2sMAC)); + packet.putString(toCommaString(s2cMAC)); + packet.putString(toCommaString(c2sComp)); + packet.putString(toCommaString(s2cComp)); + packet.putString(""); + packet.putString(""); + + packet.putBoolean(false); // Optimistic next packet does not follow + packet.putInt(0); // "Reserved" for future by spec + } + + public Proposal(SSHPacket packet) { + this.packet = packet; + final int savedPos = packet.rpos(); + packet.rpos(packet.rpos() + 17); // Skip message ID & cookie + kex = fromCommaString(packet.readString()); + sig = fromCommaString(packet.readString()); + c2sCipher = fromCommaString(packet.readString()); + s2cCipher = fromCommaString(packet.readString()); + c2sMAC = fromCommaString(packet.readString()); + s2cMAC = fromCommaString(packet.readString()); + c2sComp = fromCommaString(packet.readString()); + s2cComp = fromCommaString(packet.readString()); + packet.rpos(savedPos); + } + + public List getKeyExchangeAlgorithms() { + return kex; + } + + public List getSignatureAlgorithms() { + return sig; + } + + public List getClient2ServerCipherAlgorithms() { + return c2sCipher; + } + + public List getServer2ClientCipherAlgorithms() { + return s2cCipher; + } + + public List getClient2ServerMACAlgorithms() { + return c2sMAC; + } + + public List getServer2ClientMACAlgorithms() { + return s2cMAC; + } + + public List getClient2ServerCompressionAlgorithms() { + return c2sComp; + } + + public List getServer2ClientCompressionAlgorithms() { + return s2cComp; + } + + public SSHPacket getPacket() { + return new SSHPacket(packet); + + } + + public NegotiatedAlgorithms negotiate(Proposal other) throws TransportException { + return new NegotiatedAlgorithms( + firstMatch(this.getKeyExchangeAlgorithms(), other.getKeyExchangeAlgorithms()), // + firstMatch(this.getSignatureAlgorithms(), other.getSignatureAlgorithms()), // + firstMatch(this.getClient2ServerCipherAlgorithms(), other.getClient2ServerCipherAlgorithms()), // + firstMatch(this.getServer2ClientCipherAlgorithms(), other.getServer2ClientCipherAlgorithms()), // + firstMatch(this.getClient2ServerMACAlgorithms(), other.getClient2ServerMACAlgorithms()), // + firstMatch(this.getServer2ClientMACAlgorithms(), other.getServer2ClientMACAlgorithms()), // + firstMatch(this.getClient2ServerCompressionAlgorithms(), other.getClient2ServerCompressionAlgorithms()), // + firstMatch(this.getServer2ClientCompressionAlgorithms(), other.getServer2ClientCompressionAlgorithms()) // + ); + } + + private static String firstMatch(List a, List b) throws TransportException { + for (String aa : a) + for (String bb : b) + if (aa.equals(bb)) + return aa; + throw new TransportException("Unable to reach a settlement: " + a + " and " + b); + } + + private static String toCommaString(List sl) { + StringBuilder sb = new StringBuilder(); + int i = 0; + for (String s : sl) { + if (i++ != 0) + sb.append(","); + sb.append(s); + } + return sb.toString(); + } + + private static List fromCommaString(String s) { + return Arrays.asList(s.split(",")); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/Reader.java b/src/main/java/net/schmizz/sshj/transport/Reader.java new file mode 100644 index 00000000..a6fcee91 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Reader.java @@ -0,0 +1,86 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; + +final class Reader extends Thread { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final TransportProtocol trans; + + Reader(TransportProtocol trans) { + this.trans = trans; + setName("reader"); + } + + @Override + public void run() { + final Thread curThread = Thread.currentThread(); + + try { + + final Decoder decoder = trans.getDecoder(); + final InputStream inp = trans.getConnInfo().in; + + final byte[] recvbuf = new byte[decoder.getMaxPacketLength()]; + + int needed = 1; + + while (!curThread.isInterrupted()) { + int read = inp.read(recvbuf, 0, needed); + if (read == -1) + throw new TransportException("Broken transport; encountered EOF"); + else + needed = decoder.received(recvbuf, read); + } + + } catch (Exception e) { + if (curThread.isInterrupted()) { + // We are meant to shut up and draw to a close if interrupted + } else + trans.die(e); + } + + log.debug("Stopping"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/Transport.java b/src/main/java/net/schmizz/sshj/transport/Transport.java new file mode 100644 index 00000000..b41f94d2 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/Transport.java @@ -0,0 +1,190 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.sshj.Config; +import net.schmizz.sshj.Service; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.common.SSHPacketHandler; +import net.schmizz.sshj.transport.verification.HostKeyVerifier; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** Transport layer of the SSH protocol. */ +public interface Transport extends SSHPacketHandler { + + /** + * Sets the {@code socket} to be used by this transport; and identification information is exchanged. A {@link + * TransportException} is thrown in case of SSH protocol version incompatibility. + * + * @throws TransportException if there is an error during exchange of identification information + */ + void init(String host, int port, InputStream in, OutputStream out) throws TransportException; + + void addHostKeyVerifier(HostKeyVerifier hkv); + + void doKex() throws TransportException; + + /** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */ + String getClientVersion(); + + /** @return the {@link net.schmizz.sshj.ConfigImpl} associated with this transport. */ + Config getConfig(); + + /** @return the timeout that is currently set for blocking operations. */ + int getTimeout(); + + /** + * Set a timeout for method that may block, e.g. {@link #reqService(net.schmizz.sshj.Service)}, {@link + * KeyExchanger#waitForDone()}. + * + * @param timeout the timeout in seconds + */ + void setTimeout(int timeout); + + int getHeartbeatInterval(); + + void setHeartbeatInterval(int interval); + + /** Returns the hostname to which this transport is connected. */ + String getRemoteHost(); + + /** Returns the port number on the {@link #getRemoteHost() remote host} to which this transport is connected. */ + int getRemotePort(); + + /** + * Returns the version string as sent by the SSH server for identification purposes, e.g. "OpenSSH_$version". + *

+ * If the transport has not yet been initialized via {@link #init}, it will be {@code null}. + * + * @return server's version string (may be {@code null}) + */ + String getServerVersion(); + + byte[] getSessionID(); + + /** Returns the currently active {@link net.schmizz.sshj.Service} instance. */ + Service getService(); + + /** + * Request a SSH service represented by a {@link net.schmizz.sshj.Service} instance. A separate call to {@link + * #setService} is not needed. + * + * @param service the SSH service to be requested + * + * @throws IOException if the request failed for any reason + */ + void reqService(Service service) throws TransportException; + + /** + * Sets the currently active {@link net.schmizz.sshj.Service}. Handling of non-transport-layer packets is {@link + * net.schmizz.sshj.Service#handle delegated} to that service. + *

+ * For this method to be successful, at least one service request via {@link #reqService} must have been successful + * (not necessarily for the service being set). + * + * @param service (null-ok) the {@link net.schmizz.sshj.Service} + */ + void setService(Service service); + + /** Returns whether the transport thinks it is authenticated. */ + boolean isAuthenticated(); + + /** + * Informs this transport that authentication has been completed. This method must be called after + * successful authentication, so that delayed compression may become effective if applicable. + */ + void setAuthenticated(); + + /** + * Sends SSH_MSG_UNIMPLEMENTED in response to the last packet received. + * + * @return the sequence number of the packet sent + * + * @throws TransportException if an error occured sending the packet + */ + long sendUnimplemented() throws TransportException; + + /** + * Returns whether this transport is active. + *

+ * The transport is considered to be running if it has been initialized without error via {@link #init} and has not + * been disconnected. + */ + boolean isRunning(); + + /** + * Joins the thread calling this method to the transport's death. The transport dies of exceptional events. + * + * @throws TransportException + */ + void join() throws TransportException; + + /** Send a disconnection packet with reason as {@link DisconnectReason#BY_APPLICATION}, and closes this transport. */ + void disconnect(); + + /** + * Send a disconnect packet with the given {@link net.schmizz.sshj.common.DisconnectReason reason}, and closes this + * transport. + */ + void disconnect(DisconnectReason reason); + + /** + * Send a disconnect packet with the given {@link DisconnectReason reason} and {@code message}, and closes this + * transport. + * + * @param reason the reason code for this disconnect + * @param message the text message + */ + void disconnect(DisconnectReason reason, String message); + + /** + * Write a packet over this transport. + *

+ * The {@code payload} {@link net.schmizz.sshj.common.SSHPacket} should have 5 bytes free at the beginning to avoid + * a performance penalty associated with making space for header bytes (packet length, padding length). + * + * @param payload the {@link net.schmizz.sshj.common.SSHPacket} containing data to send + * + * @return sequence number of the sent packet + * + * @throws TransportException if an error occurred sending the packet + */ + long write(SSHPacket payload) throws TransportException; +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/TransportException.java b/src/main/java/net/schmizz/sshj/transport/TransportException.java new file mode 100644 index 00000000..d079afac --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/TransportException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.concurrent.ExceptionChainer; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHException; + +/** Transport-layer exception */ +public class TransportException extends SSHException { + + /** @see {@link net.schmizz.concurrent.ExceptionChainer} */ + public static final ExceptionChainer chainer = new ExceptionChainer() { + public TransportException chain(Throwable t) { + if (t instanceof TransportException) + return (TransportException) t; + else + return new TransportException(t); + } + }; + + public TransportException() { + super(); + } + + public TransportException(DisconnectReason code) { + super(code); + } + + public TransportException(DisconnectReason code, String message) { + super(code, message); + } + + public TransportException(DisconnectReason code, String message, Throwable cause) { + super(code, message, cause); + } + + public TransportException(DisconnectReason code, Throwable cause) { + super(code, cause); + } + + public TransportException(String message) { + super(message); + } + + public TransportException(String message, Throwable cause) { + super(message, cause); + } + + public TransportException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/TransportProtocol.java b/src/main/java/net/schmizz/sshj/transport/TransportProtocol.java new file mode 100644 index 00000000..d12c231a --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/TransportProtocol.java @@ -0,0 +1,548 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport; + +import net.schmizz.concurrent.Event; +import net.schmizz.concurrent.FutureUtils; +import net.schmizz.sshj.AbstractService; +import net.schmizz.sshj.Config; +import net.schmizz.sshj.Service; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.verification.HostKeyVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +/** A thread-safe {@link Transport} implementation. */ +public final class TransportProtocol implements Transport { + + private static final class NullService extends AbstractService { + NullService(Transport trans) { + super("null-service", trans); + } + } + + static final class ConnInfo { + final String host; + final int port; + final InputStream in; + final OutputStream out; + + public ConnInfo(String host, int port, InputStream in, OutputStream out) { + this.host = host; + this.port = port; + this.in = in; + this.out = out; + } + } + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final Service nullService = new NullService(this); + + private final Config config; + + private final KeyExchanger kexer; + + private final Reader reader; + + private final Heartbeater heartbeater; + + private final Encoder encoder; + + private final Decoder decoder; + + private final Event serviceAccept = new Event("service accept", + TransportException.chainer); + + private final Event close = new Event("transport close", + TransportException.chainer); + + /** Client version identification string */ + private final String clientID; + + private volatile int timeout = 30; + + private volatile boolean authed = false; + + /** Currently active service e.g. UserAuthService, ConnectionService */ + private volatile Service service = nullService; + + private ConnInfo connInfo; + + /** Server version identification string */ + private String serverID; + + /** Message identifier of last packet received */ + private Message msg; + + private final ReentrantLock writeLock = new ReentrantLock(); + + public TransportProtocol(Config config) { + this.config = config; + this.reader = new Reader(this); + this.heartbeater = new Heartbeater(this); + this.encoder = new Encoder(config.getRandomFactory().create(), writeLock); + this.decoder = new Decoder(this); + this.kexer = new KeyExchanger(this); + clientID = "SSH-2.0-" + config.getVersion(); + } + + public void init(String remoteHost, int remotePort, InputStream in, OutputStream out) throws TransportException { + connInfo = new ConnInfo(remoteHost, remotePort, in, out); + + try { + + log.info("Client identity string: {}", clientID); + connInfo.out.write((clientID + "\r\n").getBytes()); + + // Read server's ID + final Buffer.PlainBuffer buf = new Buffer.PlainBuffer(); + while ((serverID = readIdentification(buf)).isEmpty()) { + buf.putByte((byte) connInfo.in.read()); + } + + log.info("Server identity string: {}", serverID); + + } catch (IOException e) { + throw new TransportException(e); + } + + reader.start(); + } + + /** + * Reads the identification string from the SSH server. This is the very first string that is sent upon connection + * by the server. It takes the form of, e.g. "SSH-2.0-OpenSSH_ver". + *

+ * Several concerns are taken care of here, e.g. verifying protocol version, correct line endings as specified in + * RFC and such. + *

+ * This is not efficient but is only done once. + * + * @param buffer + * + * @return + * + * @throws IOException + */ + private String readIdentification(Buffer.PlainBuffer buffer) throws IOException { + String ident; + + byte[] data = new byte[256]; + for (; ;) { + int savedBufPos = buffer.rpos(); + int pos = 0; + boolean needLF = false; + for (; ;) { + if (buffer.available() == 0) { + // Need more data, so undo reading and return null + buffer.rpos(savedBufPos); + return ""; + } + byte b = buffer.readByte(); + if (b == '\r') { + needLF = true; + continue; + } + if (b == '\n') + break; + if (needLF) + throw new TransportException("Incorrect identification: bad line ending"); + if (pos >= data.length) + throw new TransportException("Incorrect identification: line too long"); + data[pos++] = b; + } + ident = new String(data, 0, pos); + if (ident.startsWith("SSH-")) + break; + if (buffer.rpos() > 16 * 1024) + throw new TransportException("Incorrect identification: too many header lines"); + } + + if (!ident.startsWith("SSH-2.0-") && !ident.startsWith("SSH-1.99-")) + throw new TransportException(DisconnectReason.PROTOCOL_VERSION_NOT_SUPPORTED, + "Server does not support SSHv2, identified as: " + ident); + + return ident; + } + + public void addHostKeyVerifier(HostKeyVerifier hkv) { + kexer.addHostKeyVerifier(hkv); + } + + public void doKex() throws TransportException { + kexer.startKex(true); + } + + public boolean isKexDone() { + return kexer.isKexDone(); + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public int getHeartbeatInterval() { + return heartbeater.getInterval(); + } + + public void setHeartbeatInterval(int interval) { + heartbeater.setInterval(interval); + } + + public String getRemoteHost() { + return connInfo.host; + } + + public int getRemotePort() { + return connInfo.port; + } + + public String getClientVersion() { + return clientID.substring(8); + } + + public Config getConfig() { + return config; + } + + public String getServerVersion() { + return serverID == null ? serverID : serverID.substring(8); + } + + public byte[] getSessionID() { + return kexer.getSessionID(); + } + + public synchronized Service getService() { + return service; + } + + public synchronized void setService(Service service) { + if (service == null) + service = nullService; + + log.info("Setting active service to {}", service.getName()); + this.service = service; + } + + public void reqService(Service service) throws TransportException { + serviceAccept.lock(); + try { + serviceAccept.clear(); + sendServiceRequest(service.getName()); + serviceAccept.await(timeout, TimeUnit.SECONDS); + setService(service); + } finally { + serviceAccept.unlock(); + } + } + + /** + * Sends a service request for the specified service + * + * @param serviceName name of the service being requested + * + * @throws TransportException if there is an error while sending the request + */ + private void sendServiceRequest(String serviceName) throws TransportException { + log.debug("Sending SSH_MSG_SERVICE_REQUEST for {}", serviceName); + write(new SSHPacket(Message.SERVICE_REQUEST).putString(serviceName)); + } + + public void setAuthenticated() { + this.authed = true; + encoder.setAuthenticated(); + decoder.setAuthenticated(); + } + + public boolean isAuthenticated() { + return authed; + } + + public long sendUnimplemented() throws TransportException { + final long seq = decoder.getSequenceNumber(); + log.info("Sending SSH_MSG_UNIMPLEMENTED for packet #{}", seq); + return write(new SSHPacket(Message.UNIMPLEMENTED).putInt(seq)); + } + + public void join() throws TransportException { + close.await(); + } + + public boolean isRunning() { + return reader.isAlive() && !close.isSet(); + } + + public void disconnect() { + disconnect(DisconnectReason.BY_APPLICATION); + } + + public void disconnect(DisconnectReason reason) { + disconnect(reason, ""); + } + + public void disconnect(DisconnectReason reason, String message) { + close.lock(); // CAS type operation on close + try { + try { + service.notifyDisconnect(); + } catch (SSHException logged) { + log.warn("{} did not handle disconnect cleanly: {}", service, logged); + } + if (!close.isSet()) { + sendDisconnect(reason, message); + finishOff(); + close.set(); + } + } finally { + close.unlock(); + } + } + + public long write(SSHPacket payload) throws TransportException { + writeLock.lock(); + try { + + if (kexer.isKexOngoing()) { + // Only transport layer packets (1 to 49) allowed except SERVICE_REQUEST + final Message m = Message.fromByte(payload.array()[payload.rpos()]); + if (!m.in(1, 49) || m == Message.SERVICE_REQUEST) { + assert m != Message.KEXINIT; + kexer.waitForDone(); + } + } else if (encoder.getSequenceNumber() == 0) // We get here every 2**32th packet + kexer.startKex(true); + + final long seq = encoder.encode(payload); + try { + connInfo.out.write(payload.array(), payload.rpos(), payload.available()); + connInfo.out.flush(); + } catch (IOException ioe) { + throw new TransportException(ioe); + } + + return seq; + + } finally { + writeLock.unlock(); + } + } + + private void sendDisconnect(DisconnectReason reason, String message) { + if (message == null) + message = ""; + log.debug("Sending SSH_MSG_DISCONNECT: reason=[{}], msg=[{}]", reason, message); + try { + write(new SSHPacket(Message.DISCONNECT) + .putInt(reason.toInt()) + .putString(message) + .putString("")); + } catch (IOException logged) { + log.warn("Error writing packet: {}", logged); + } + } + + /** + * This is where all incoming packets are handled. If they pertain to the transport layer, they are handled here; + * otherwise they are delegated to the active service instance if any via {@link Service#handle}. + *

+ * Even among the transport layer specific packets, key exchange packets are delegated to {@link + * KeyExchanger#handle}. + *

+ * This method is called in the context of the {@link #reader} thread via {@link Decoder#received} when a full + * packet has been decoded. + * + * @param msg the message identifer + * @param buf buffer containg rest of the packet + * + * @throws SSHException if an error occurs during handling (unrecoverable) + */ + public void handle(Message msg, SSHPacket buf) throws SSHException { + this.msg = msg; + + log.trace("Received packet {}", msg); + + if (msg.geq(50)) // not a transport layer packet + service.handle(msg, buf); + + else if (msg.in(20, 21) || msg.in(30, 49)) // kex packet + kexer.handle(msg, buf); + + else + switch (msg) { + case DISCONNECT: { + gotDisconnect(buf); + break; + } + case IGNORE: { + log.info("Received SSH_MSG_IGNORE"); + break; + } + case UNIMPLEMENTED: { + gotUnimplemented(buf); + break; + } + case DEBUG: { + gotDebug(buf); + break; + } + case SERVICE_ACCEPT: { + gotServiceAccept(); + break; + } + default: + sendUnimplemented(); + } + } + + private void gotDebug(SSHPacket buf) { + boolean display = buf.readBoolean(); + String message = buf.readString(); + log.info("Received SSH_MSG_DEBUG (display={}) '{}'", display, message); + } + + private void gotDisconnect(SSHPacket buf) throws TransportException { + DisconnectReason code = DisconnectReason.fromInt(buf.readInt()); + String message = buf.readString(); + log.info("Received SSH_MSG_DISCONNECT (reason={}, msg={})", code, message); + throw new TransportException(code, "Disconnected; server said: " + message); + } + + private void gotServiceAccept() throws TransportException { + serviceAccept.lock(); + try { + if (!serviceAccept.hasWaiters()) + throw new TransportException(DisconnectReason.PROTOCOL_ERROR, + "Got a service accept notification when none was awaited"); + serviceAccept.set(); + } finally { + serviceAccept.unlock(); + } + } + + /** + * Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly. + * + * @param buf + * + * @throws TransportException + */ + private void gotUnimplemented(SSHPacket buf) throws SSHException { + long seqNum = buf.readLong(); + log.info("Received SSH_MSG_UNIMPLEMENTED #{}", seqNum); + if (kexer.isKexOngoing()) + throw new TransportException("Received SSH_MSG_UNIMPLEMENTED while exchanging keys"); + getService().notifyUnimplemented(seqNum); + } + + private void finishOff() { + reader.interrupt(); + heartbeater.interrupt(); + IOUtils.closeQuietly(connInfo.in); + IOUtils.closeQuietly(connInfo.out); + } + + void die(Exception ex) { + close.lock(); + try { + if (!close.isSet()) { + + log.error("Dying because - {}", ex.toString()); + + final SSHException causeOfDeath = SSHException.chainer.chain(ex); + + FutureUtils.alertAll(causeOfDeath, close, serviceAccept); + kexer.notifyError(causeOfDeath); + getService().notifyError(causeOfDeath); + setService(nullService); + + { // Perhaps can send disconnect packet to server + final boolean didNotReceiveDisconnect = msg != Message.DISCONNECT; + final boolean gotRequiredInfo = causeOfDeath.getDisconnectReason() != DisconnectReason.UNKNOWN; + if (didNotReceiveDisconnect && gotRequiredInfo) + sendDisconnect(causeOfDeath.getDisconnectReason(), causeOfDeath.getMessage()); + } + + finishOff(); + + close.set(); + } + } finally { + close.unlock(); + } + } + + String getClientID() { + return clientID; + } + + String getServerID() { + return serverID; + } + + Encoder getEncoder() { + return encoder; + } + + Decoder getDecoder() { + return decoder; + } + + ReentrantLock getWriteLock() { + return writeLock; + } + + ConnInfo getConnInfo() { + return connInfo; + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java new file mode 100644 index 00000000..55b70286 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** AES128CBC cipher */ +public class AES128CBC extends BaseCipher { + + /** Named factory for AES128CBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new AES128CBC(); + } + + public String getName() { + return "aes128-cbc"; + } + } + + public AES128CBC() { + super(16, 16, "AES", "AES/CBC/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java new file mode 100644 index 00000000..3629afa1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code aes128-ctr} cipher */ +public class AES128CTR extends BaseCipher { + + /** Named factory for AES128CBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new AES128CTR(); + } + + public String getName() { + return "aes128-ctr"; + } + } + + public AES128CTR() { + super(16, 16, "AES", "AES/CTR/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java new file mode 100644 index 00000000..f539b8c6 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code aes192-cbc} cipher */ +public class AES192CBC extends BaseCipher { + + /** Named factory for AES192CBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new AES192CBC(); + } + + public String getName() { + return "aes192-cbc"; + } + } + + public AES192CBC() { + super(16, 24, "AES", "AES/CBC/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java new file mode 100644 index 00000000..00df71d5 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code aes192-ctr} cipher */ +public class AES192CTR extends BaseCipher { + + /** Named factory for AES192CTR Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new AES192CTR(); + } + + public String getName() { + return "aes192-ctr"; + } + } + + public AES192CTR() { + super(16, 24, "AES", "AES/CTR/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java new file mode 100644 index 00000000..f2213e97 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code aes256-ctr} cipher */ +public class AES256CBC extends BaseCipher { + + /** Named factory for AES256CBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new AES256CBC(); + } + + public String getName() { + return "aes256-cbc"; + } + } + + public AES256CBC() { + super(16, 32, "AES", "AES/CBC/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java new file mode 100644 index 00000000..653f5576 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code aes256-ctr} cipher */ +public class AES256CTR extends BaseCipher { + + /** Named factory for AES256CBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new AES256CTR(); + } + + public String getName() { + return "aes256-ctr"; + } + } + + public AES256CTR() { + super(16, 32, "AES", "AES/CTR/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java b/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java new file mode 100644 index 00000000..bc41d92a --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java @@ -0,0 +1,101 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; + +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; + +/** Base class for all Cipher implementations delegating to the JCE provider. */ +public class BaseCipher implements Cipher { + + private static byte[] resize(byte[] data, int size) { + if (data.length > size) { + final byte[] tmp = new byte[size]; + System.arraycopy(data, 0, tmp, 0, size); + data = tmp; + } + return data; + } + + private final int ivsize; + private final int bsize; + private final String algorithm; + private final String transformation; + + private javax.crypto.Cipher cipher; + + public BaseCipher(int ivsize, int bsize, String algorithm, String transformation) { + this.ivsize = ivsize; + this.bsize = bsize; + this.algorithm = algorithm; + this.transformation = transformation; + } + + public int getBlockSize() { + return bsize; + } + + public int getIVSize() { + return ivsize; + } + + public void init(Mode mode, byte[] key, byte[] iv) { + key = BaseCipher.resize(key, bsize); + iv = BaseCipher.resize(iv, ivsize); + try { + cipher = SecurityUtils.getCipher(transformation); + cipher.init((mode == Mode.Encrypt ? javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE), + new SecretKeySpec(key, algorithm), new IvParameterSpec(iv)); + } catch (GeneralSecurityException e) { + cipher = null; + throw new SSHRuntimeException(e); + } + } + + public void update(byte[] input, int inputOffset, int inputLen) { + try { + cipher.update(input, inputOffset, inputLen, input, inputOffset); + } catch (ShortBufferException e) { + throw new SSHRuntimeException(e); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java new file mode 100644 index 00000000..8212531b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code blowfish-ctr} cipher */ +public class BlowfishCBC extends BaseCipher { + + /** Named factory for BlowfishCBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new BlowfishCBC(); + } + + public String getName() { + return "blowfish-cbc"; + } + } + + public BlowfishCBC() { + super(8, 16, "Blowfish", "Blowfish/CBC/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java b/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java new file mode 100644 index 00000000..3a4a8824 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java @@ -0,0 +1,77 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** Wrapper for a cryptographic cipher, used either for encryption or decryption. */ +public interface Cipher { + + enum Mode { + Encrypt, Decrypt + } + + /** + * Retrieves the block size for this cipher + * + * @return + */ + int getBlockSize(); + + /** + * Retrieves the size of the initialization vector + * + * @return + */ + int getIVSize(); + + /** + * Initialize the cipher for encryption or decryption with the given private key and initialization vector + * + * @param mode + * @param key + * @param iv + */ + void init(Mode mode, byte[] key, byte[] iv); + + /** + * Performs in-place encryption or decryption on the given data. + * + * @param input + * @param inputOffset + * @param inputLen + */ + void update(byte[] input, int inputOffset, int inputLen); + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java b/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java new file mode 100644 index 00000000..d930049b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java @@ -0,0 +1,66 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** Represents a no-op cipher. */ +public class NoneCipher implements Cipher { + + /** Named factory for the no-op Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new NoneCipher(); + } + + public String getName() { + return "none"; + } + } + + public int getBlockSize() { + return 8; + } + + public int getIVSize() { + return 8; + } + + public void init(Mode mode, byte[] bytes, byte[] bytes1) { + } + + public void update(byte[] input, int inputOffset, int inputLen) { + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java new file mode 100644 index 00000000..0538272a --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.cipher; + +/** {@code 3des-cbc} cipher */ +public class TripleDESCBC extends BaseCipher { + + /** Named factory for TripleDESCBC Cipher */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Cipher create() { + return new TripleDESCBC(); + } + + public String getName() { + return "3des-cbc"; + } + } + + public TripleDESCBC() { + super(8, 24, "DESede", "DESede/CBC/NoPadding"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/compression/Compression.java b/src/main/java/net/schmizz/sshj/transport/compression/Compression.java new file mode 100644 index 00000000..fa8a31e4 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/compression/Compression.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.compression; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.TransportException; + +/** Interface used to compress the stream of data between the SSH server and clients. */ +public interface Compression { + + /** Enum identifying if this object will be used to compress or uncompress data. */ + enum Type { + Inflater, Deflater + } + + /** + * Compress the given buffer in place. + * + * @param buffer the buffer containing the data to compress s + */ + void compress(SSHPacket buffer); + + /** + * Initialize this object to either compress or uncompress data. This method must be called prior to any calls to + * either compress or uncompress. Once the object has been initialized, only one of + * compress or uncompress method can be called. + * + * @param type + * @param level + */ + void init(Type type, int level); + + /** + * Delayed compression is an Open-SSH specific feature which informs both the client and server to not compress data + * before the session has been authenticated. + * + * @return if the compression is delayed after authentication or not + */ + boolean isDelayed(); + + /** + * Uncompress the data in a buffer into another buffer. + * + * @param from the buffer containing the data to uncompress + * @param to the buffer receiving the uncompressed data + */ + void uncompress(SSHPacket from, SSHPacket to) throws TransportException; + +} diff --git a/src/main/java/net/schmizz/sshj/transport/compression/DelayedZlibCompression.java b/src/main/java/net/schmizz/sshj/transport/compression/DelayedZlibCompression.java new file mode 100644 index 00000000..30991e63 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/compression/DelayedZlibCompression.java @@ -0,0 +1,61 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.compression; + +/** + * ZLib delayed compression. + * + * @see Compression#isDelayed() + */ +public class DelayedZlibCompression extends ZlibCompression { + + /** Named factory for the ZLib Delayed Compression. */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Compression create() { + return new DelayedZlibCompression(); + } + + public String getName() { + return "zlib@openssh.com"; + } + } + + @Override + public boolean isDelayed() { + return true; + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/compression/NoneCompression.java b/src/main/java/net/schmizz/sshj/transport/compression/NoneCompression.java new file mode 100644 index 00000000..cb1e138e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/compression/NoneCompression.java @@ -0,0 +1,52 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.compression; + +/** No-op Compression. */ +public abstract class NoneCompression implements Compression { + + /** Named factory for the no-op Compression */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Compression create() { + return null; + } + + public String getName() { + return "none"; + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/compression/ZlibCompression.java b/src/main/java/net/schmizz/sshj/transport/compression/ZlibCompression.java new file mode 100644 index 00000000..ac1dcbea --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/compression/ZlibCompression.java @@ -0,0 +1,123 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.compression; + +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZStream; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.transport.TransportException; + +/** ZLib based Compression. */ +public class ZlibCompression implements Compression { + + /** Named factory for the ZLib Compression. */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public Compression create() { + return new ZlibCompression(); + } + + public String getName() { + return "zlib"; + } + } + + static private final int BUF_SIZE = 4096; + + private ZStream stream; + private final byte[] tmpbuf = new byte[BUF_SIZE]; + + /** Create a new instance of a ZLib base compression */ + public ZlibCompression() { + } + + public void compress(SSHPacket buffer) { + stream.next_in = buffer.array(); + stream.next_in_index = buffer.rpos(); + stream.avail_in = buffer.available(); + buffer.wpos(buffer.rpos()); + do { + stream.next_out = tmpbuf; + stream.next_out_index = 0; + stream.avail_out = BUF_SIZE; + int status = stream.deflate(JZlib.Z_PARTIAL_FLUSH); + switch (status) { + case JZlib.Z_OK: + buffer.putRawBytes(tmpbuf, 0, BUF_SIZE - stream.avail_out); + break; + default: + throw new SSHRuntimeException("compress: deflate returned " + status); + } + } while (stream.avail_out == 0); + } + + public void init(Type type, int level) { + stream = new ZStream(); + if (type == Type.Deflater) + stream.deflateInit(level); + else + stream.inflateInit(); + } + + public boolean isDelayed() { + return false; + } + + public void uncompress(SSHPacket from, SSHPacket to) throws TransportException { + stream.next_in = from.array(); + stream.next_in_index = from.rpos(); + stream.avail_in = from.available(); + + while (true) { + stream.next_out = tmpbuf; + stream.next_out_index = 0; + stream.avail_out = BUF_SIZE; + int status = stream.inflate(JZlib.Z_PARTIAL_FLUSH); + switch (status) { + case JZlib.Z_OK: + to.putRawBytes(tmpbuf, 0, BUF_SIZE - stream.avail_out); + break; + case JZlib.Z_BUF_ERROR: + return; // wtf.. but this works *head spins* + default: + throw new TransportException(DisconnectReason.COMPRESSION_ERROR, "uncompress: inflate returned " + + status); + } + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/digest/BaseDigest.java b/src/main/java/net/schmizz/sshj/transport/digest/BaseDigest.java new file mode 100644 index 00000000..42cdebd7 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/digest/BaseDigest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.digest; + +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; + +import java.security.GeneralSecurityException; +import java.security.MessageDigest; + +/** Base class for Digest algorithms based on the JCE provider. */ +public class BaseDigest implements Digest { + + private final String algorithm; + private final int bsize; + private MessageDigest md; + + /** + * Create a new digest using the given algorithm and block size. The initialization and creation of the underlying + * {@link MessageDigest} object will be done in the {@link #init()} method. + * + * @param algorithm the JCE algorithm to use for this digest + * @param bsize the block size of this digest + */ + public BaseDigest(String algorithm, int bsize) { + this.algorithm = algorithm; + this.bsize = bsize; + } + + public byte[] digest() { + return md.digest(); + } + + public int getBlockSize() { + return bsize; + } + + public void init() { + try { + md = SecurityUtils.getMessageDigest(algorithm); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + } + + public void update(byte[] foo) { + update(foo, 0, foo.length); + } + + public void update(byte[] foo, int start, int len) { + md.update(foo, start, len); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/digest/Digest.java b/src/main/java/net/schmizz/sshj/transport/digest/Digest.java new file mode 100644 index 00000000..5541d3c3 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/digest/Digest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.digest; + +/** Interface used to compute digests, based on algorithms such as MD5 or SHA1. */ +public interface Digest { + + byte[] digest(); + + int getBlockSize(); + + void init(); + + void update(byte[] foo); + + void update(byte[] foo, int start, int len); + +} diff --git a/src/main/java/net/schmizz/sshj/transport/digest/MD5.java b/src/main/java/net/schmizz/sshj/transport/digest/MD5.java new file mode 100644 index 00000000..2738fbf2 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/digest/MD5.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.digest; + +/** MD5 Digest. */ +public class MD5 extends BaseDigest { + + /** Named factory for MD5 digest */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public Digest create() { + return new MD5(); + } + + public String getName() { + return "md5"; + } + } + + /** Create a new instance of a MD5 digest */ + public MD5() { + super("MD5", 16); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/digest/SHA1.java b/src/main/java/net/schmizz/sshj/transport/digest/SHA1.java new file mode 100644 index 00000000..07917171 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/digest/SHA1.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.digest; + +/** SHA1 Digest. */ +public class SHA1 extends BaseDigest { + + /** Named factory for SHA1 digest */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public Digest create() { + return new SHA1(); + } + + public String getName() { + return "sha1"; + } + } + + /** Create a new instance of a SHA1 digest */ + public SHA1() { + super("SHA-1", 20); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java b/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java new file mode 100644 index 00000000..24e6e46e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java @@ -0,0 +1,144 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.kex; + +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.signature.Signature; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.transport.digest.Digest; +import net.schmizz.sshj.transport.digest.SHA1; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.PublicKey; + +/** + * Base class for DHG key exchange algorithms. Implementations will only have to configure the required data on the + * {@link DH} class in the + */ +public abstract class AbstractDHG implements KeyExchange { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private Transport trans; + + private final Digest sha = new SHA1(); + private final DH dh = new DH(); + + private byte[] V_S; + private byte[] V_C; + private byte[] I_S; + private byte[] I_C; + + private byte[] e; + private byte[] f; + private byte[] K; + private byte[] H; + private PublicKey hostKey; + + public byte[] getH() { + return ByteArrayUtils.copyOf(H); + } + + public Digest getHash() { + return sha; + } + + public PublicKey getHostKey() { + return hostKey; + } + + public byte[] getK() { + return ByteArrayUtils.copyOf(K); + } + + public void init(Transport trans, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C) throws TransportException { + this.trans = trans; + this.V_S = ByteArrayUtils.copyOf(V_S); + this.V_C = ByteArrayUtils.copyOf(V_C); + this.I_S = ByteArrayUtils.copyOf(I_S); + this.I_C = ByteArrayUtils.copyOf(I_C); + sha.init(); + initDH(dh); + e = dh.getE(); + + log.info("Sending SSH_MSG_KEXDH_INIT"); + trans.write(new SSHPacket(Message.KEXDH_INIT).putMPInt(e)); + } + + public boolean next(Message msg, SSHPacket packet) throws TransportException { + if (msg != Message.KEXDH_31) + throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED, "Unxpected packet: " + msg); + + log.info("Received SSH_MSG_KEXDH_REPLY"); + final byte[] K_S = packet.readBytes(); + f = packet.readMPIntAsBytes(); + final byte[] sig = packet.readBytes(); // signature sent by server + dh.setF(f); + K = dh.getK(); + + hostKey = new Buffer.PlainBuffer(K_S).readPublicKey(); + + final Buffer.PlainBuffer buf = new Buffer.PlainBuffer() // our hash + .putString(V_C) // + .putString(V_S) // + .putString(I_C) // + .putString(I_S) // + .putString(K_S) // + .putMPInt(e) // + .putMPInt(f) // + .putMPInt(K); // + sha.update(buf.array(), 0, buf.available()); + H = sha.digest(); + + Signature signature = Factory.Named.Util.create(trans.getConfig().getSignatureFactories(), KeyType.fromKey(hostKey).toString()); + signature.init(hostKey, null); + signature.update(H, 0, H.length); + if (!signature.verify(sig)) + throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED, "KeyExchange signature verification failed"); + return true; + } + + protected abstract void initDH(DH dh); + +} diff --git a/src/main/java/net/schmizz/sshj/transport/kex/DH.java b/src/main/java/net/schmizz/sshj/transport/kex/DH.java new file mode 100644 index 00000000..d83e98b0 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/kex/DH.java @@ -0,0 +1,133 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.kex; + +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; + +import javax.crypto.KeyAgreement; +import javax.crypto.spec.DHParameterSpec; +import javax.crypto.spec.DHPublicKeySpec; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; + +/** Diffie-Hellman key generator. */ +public class DH { + + private BigInteger p; + private BigInteger g; + private BigInteger e; // my public key + private byte[] e_array; + private BigInteger f; // your public key + private BigInteger K; // shared secret key + private byte[] K_array; + private final KeyPairGenerator myKpairGen; + private final KeyAgreement myKeyAgree; + + public DH() { + try { + myKpairGen = SecurityUtils.getKeyPairGenerator("DH"); + myKeyAgree = SecurityUtils.getKeyAgreement("DH"); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + + } + + public byte[] getE() { + if (e == null) { + DHParameterSpec dhSkipParamSpec = new DHParameterSpec(p, g); + KeyPair myKpair; + try { + myKpairGen.initialize(dhSkipParamSpec); + myKpair = myKpairGen.generateKeyPair(); + myKeyAgree.init(myKpair.getPrivate()); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + e = ((javax.crypto.interfaces.DHPublicKey) myKpair.getPublic()).getY(); + e_array = e.toByteArray(); + } + return ByteArrayUtils.copyOf(e_array); + } + + public byte[] getK() { + if (K == null) { + try { + KeyFactory myKeyFac = SecurityUtils.getKeyFactory("DH"); + DHPublicKeySpec keySpec = new DHPublicKeySpec(f, p, g); + PublicKey yourPubKey = myKeyFac.generatePublic(keySpec); + myKeyAgree.doPhase(yourPubKey, true); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + byte[] mySharedSecret = myKeyAgree.generateSecret(); + K = new BigInteger(mySharedSecret); + K_array = mySharedSecret; + } + return ByteArrayUtils.copyOf(K_array); + } + + public void setF(byte[] f) { + setF(new BigInteger(f)); + } + + public void setG(byte[] g) { + setG(new BigInteger(g)); + } + + public void setP(byte[] p) { + setP(new BigInteger(p)); + } + + void setF(BigInteger f) { + this.f = f; + } + + void setG(BigInteger g) { + this.g = g; + } + + void setP(BigInteger p) { + this.p = p; + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/kex/DHG1.java b/src/main/java/net/schmizz/sshj/transport/kex/DHG1.java new file mode 100644 index 00000000..15c7872b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/kex/DHG1.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.kex; + +/** + * Diffie-Hellman key exchange with SHA-1 and Oakley Group 2 [RFC2409] (1024-bit MODP Group). + * + * @see RFC 4253 + */ +public class DHG1 extends AbstractDHG { + + /** Named factory for DHG1 key exchange */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public KeyExchange create() { + return new DHG1(); + } + + public String getName() { + return "diffie-hellman-group1-sha1"; + } + + } + + @Override + protected void initDH(DH dh) { + dh.setG(DHGroupData.getG()); + dh.setP(DHGroupData.getP1()); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/kex/DHG14.java b/src/main/java/net/schmizz/sshj/transport/kex/DHG14.java new file mode 100644 index 00000000..9eee9d5e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/kex/DHG14.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.kex; + +/** + * Diffie-Hellman key exchange with SHA-1 and Oakley Group 14 [RFC3526] (2048-bit MODP Group). + *

+ * DHG14 does not work with the default JCE implementation provided by Sun because it does not support 2048 bits + * encryption. It requires BouncyCastle to be used. + */ +public class DHG14 extends AbstractDHG { + + /** Named factory for DHG14 key exchange */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public KeyExchange create() { + return new DHG14(); + } + + public String getName() { + return "diffie-hellman-group14-sha1"; + } + + } + + @Override + protected void initDH(DH dh) { + dh.setG(DHGroupData.getG()); + dh.setP(DHGroupData.getP14()); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/kex/DHGroupData.java b/src/main/java/net/schmizz/sshj/transport/kex/DHGroupData.java new file mode 100644 index 00000000..c4cfa649 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/kex/DHGroupData.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.kex; + +/** Simple class holding the data for DH group key exchanges. */ +public final class DHGroupData { + + public static byte[] getG() { + final byte[] G = {2}; + return G; + } + + public static byte[] getP1() { + final byte[] P_1 = {(byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xC9, (byte) 0x0F, (byte) 0xDA, (byte) 0xA2, (byte) 0x21, (byte) 0x68, + (byte) 0xC2, (byte) 0x34, (byte) 0xC4, (byte) 0xC6, (byte) 0x62, (byte) 0x8B, (byte) 0x80, (byte) 0xDC, + (byte) 0x1C, (byte) 0xD1, (byte) 0x29, (byte) 0x02, (byte) 0x4E, (byte) 0x08, (byte) 0x8A, (byte) 0x67, + (byte) 0xCC, (byte) 0x74, (byte) 0x02, (byte) 0x0B, (byte) 0xBE, (byte) 0xA6, (byte) 0x3B, (byte) 0x13, + (byte) 0x9B, (byte) 0x22, (byte) 0x51, (byte) 0x4A, (byte) 0x08, (byte) 0x79, (byte) 0x8E, (byte) 0x34, + (byte) 0x04, (byte) 0xDD, (byte) 0xEF, (byte) 0x95, (byte) 0x19, (byte) 0xB3, (byte) 0xCD, (byte) 0x3A, + (byte) 0x43, (byte) 0x1B, (byte) 0x30, (byte) 0x2B, (byte) 0x0A, (byte) 0x6D, (byte) 0xF2, (byte) 0x5F, + (byte) 0x14, (byte) 0x37, (byte) 0x4F, (byte) 0xE1, (byte) 0x35, (byte) 0x6D, (byte) 0x6D, (byte) 0x51, + (byte) 0xC2, (byte) 0x45, (byte) 0xE4, (byte) 0x85, (byte) 0xB5, (byte) 0x76, (byte) 0x62, (byte) 0x5E, + (byte) 0x7E, (byte) 0xC6, (byte) 0xF4, (byte) 0x4C, (byte) 0x42, (byte) 0xE9, (byte) 0xA6, (byte) 0x37, + (byte) 0xED, (byte) 0x6B, (byte) 0x0B, (byte) 0xFF, (byte) 0x5C, (byte) 0xB6, (byte) 0xF4, (byte) 0x06, + (byte) 0xB7, (byte) 0xED, (byte) 0xEE, (byte) 0x38, (byte) 0x6B, (byte) 0xFB, (byte) 0x5A, (byte) 0x89, + (byte) 0x9F, (byte) 0xA5, (byte) 0xAE, (byte) 0x9F, (byte) 0x24, (byte) 0x11, (byte) 0x7C, (byte) 0x4B, + (byte) 0x1F, (byte) 0xE6, (byte) 0x49, (byte) 0x28, (byte) 0x66, (byte) 0x51, (byte) 0xEC, (byte) 0xE6, + (byte) 0x53, (byte) 0x81, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF}; + return P_1; + } + + public static byte[] getP14() { + final byte[] P_14 = {(byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xC9, (byte) 0x0F, (byte) 0xDA, (byte) 0xA2, (byte) 0x21, + (byte) 0x68, (byte) 0xC2, (byte) 0x34, (byte) 0xC4, (byte) 0xC6, (byte) 0x62, (byte) 0x8B, (byte) 0x80, + (byte) 0xDC, (byte) 0x1C, (byte) 0xD1, (byte) 0x29, (byte) 0x02, (byte) 0x4E, (byte) 0x08, (byte) 0x8A, + (byte) 0x67, (byte) 0xCC, (byte) 0x74, (byte) 0x02, (byte) 0x0B, (byte) 0xBE, (byte) 0xA6, (byte) 0x3B, + (byte) 0x13, (byte) 0x9B, (byte) 0x22, (byte) 0x51, (byte) 0x4A, (byte) 0x08, (byte) 0x79, (byte) 0x8E, + (byte) 0x34, (byte) 0x04, (byte) 0xDD, (byte) 0xEF, (byte) 0x95, (byte) 0x19, (byte) 0xB3, (byte) 0xCD, + (byte) 0x3A, (byte) 0x43, (byte) 0x1B, (byte) 0x30, (byte) 0x2B, (byte) 0x0A, (byte) 0x6D, (byte) 0xF2, + (byte) 0x5F, (byte) 0x14, (byte) 0x37, (byte) 0x4F, (byte) 0xE1, (byte) 0x35, (byte) 0x6D, (byte) 0x6D, + (byte) 0x51, (byte) 0xC2, (byte) 0x45, (byte) 0xE4, (byte) 0x85, (byte) 0xB5, (byte) 0x76, (byte) 0x62, + (byte) 0x5E, (byte) 0x7E, (byte) 0xC6, (byte) 0xF4, (byte) 0x4C, (byte) 0x42, (byte) 0xE9, (byte) 0xA6, + (byte) 0x37, (byte) 0xED, (byte) 0x6B, (byte) 0x0B, (byte) 0xFF, (byte) 0x5C, (byte) 0xB6, (byte) 0xF4, + (byte) 0x06, (byte) 0xB7, (byte) 0xED, (byte) 0xEE, (byte) 0x38, (byte) 0x6B, (byte) 0xFB, (byte) 0x5A, + (byte) 0x89, (byte) 0x9F, (byte) 0xA5, (byte) 0xAE, (byte) 0x9F, (byte) 0x24, (byte) 0x11, (byte) 0x7C, + (byte) 0x4B, (byte) 0x1F, (byte) 0xE6, (byte) 0x49, (byte) 0x28, (byte) 0x66, (byte) 0x51, (byte) 0xEC, + (byte) 0xE4, (byte) 0x5B, (byte) 0x3D, (byte) 0xC2, (byte) 0x00, (byte) 0x7C, (byte) 0xB8, (byte) 0xA1, + (byte) 0x63, (byte) 0xBF, (byte) 0x05, (byte) 0x98, (byte) 0xDA, (byte) 0x48, (byte) 0x36, (byte) 0x1C, + (byte) 0x55, (byte) 0xD3, (byte) 0x9A, (byte) 0x69, (byte) 0x16, (byte) 0x3F, (byte) 0xA8, (byte) 0xFD, + (byte) 0x24, (byte) 0xCF, (byte) 0x5F, (byte) 0x83, (byte) 0x65, (byte) 0x5D, (byte) 0x23, (byte) 0xDC, + (byte) 0xA3, (byte) 0xAD, (byte) 0x96, (byte) 0x1C, (byte) 0x62, (byte) 0xF3, (byte) 0x56, (byte) 0x20, + (byte) 0x85, (byte) 0x52, (byte) 0xBB, (byte) 0x9E, (byte) 0xD5, (byte) 0x29, (byte) 0x07, (byte) 0x70, + (byte) 0x96, (byte) 0x96, (byte) 0x6D, (byte) 0x67, (byte) 0x0C, (byte) 0x35, (byte) 0x4E, (byte) 0x4A, + (byte) 0xBC, (byte) 0x98, (byte) 0x04, (byte) 0xF1, (byte) 0x74, (byte) 0x6C, (byte) 0x08, (byte) 0xCA, + (byte) 0x18, (byte) 0x21, (byte) 0x7C, (byte) 0x32, (byte) 0x90, (byte) 0x5E, (byte) 0x46, (byte) 0x2E, + (byte) 0x36, (byte) 0xCE, (byte) 0x3B, (byte) 0xE3, (byte) 0x9E, (byte) 0x77, (byte) 0x2C, (byte) 0x18, + (byte) 0x0E, (byte) 0x86, (byte) 0x03, (byte) 0x9B, (byte) 0x27, (byte) 0x83, (byte) 0xA2, (byte) 0xEC, + (byte) 0x07, (byte) 0xA2, (byte) 0x8F, (byte) 0xB5, (byte) 0xC5, (byte) 0x5D, (byte) 0xF0, (byte) 0x6F, + (byte) 0x4C, (byte) 0x52, (byte) 0xC9, (byte) 0xDE, (byte) 0x2B, (byte) 0xCB, (byte) 0xF6, (byte) 0x95, + (byte) 0x58, (byte) 0x17, (byte) 0x18, (byte) 0x39, (byte) 0x95, (byte) 0x49, (byte) 0x7C, (byte) 0xEA, + (byte) 0x95, (byte) 0x6A, (byte) 0xE5, (byte) 0x15, (byte) 0xD2, (byte) 0x26, (byte) 0x18, (byte) 0x98, + (byte) 0xFA, (byte) 0x05, (byte) 0x10, (byte) 0x15, (byte) 0x72, (byte) 0x8E, (byte) 0x5A, (byte) 0x8A, + (byte) 0xAC, (byte) 0xAA, (byte) 0x68, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}; + return P_14; + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/kex/KeyExchange.java b/src/main/java/net/schmizz/sshj/transport/kex/KeyExchange.java new file mode 100644 index 00000000..33b7a94c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/kex/KeyExchange.java @@ -0,0 +1,87 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.kex; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.transport.digest.Digest; + +import java.security.PublicKey; + +/** Key exchange algorithm. */ +public interface KeyExchange { + + /** + * Retrieves the computed H parameter + * + * @return + */ + byte[] getH(); + + /** + * The message digest used by this key exchange algorithm. + * + * @return the message digest + */ + Digest getHash(); + + PublicKey getHostKey(); + + /** Retrieves the computed K parameter */ + byte[] getK(); + + /** + * Initialize the key exchange algorithm. + * + * @param V_S the server identification string + * @param V_C the client identification string + * @param I_S the server key init packet + * @param I_C the client key init packet + */ + void init(Transport trans, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C) throws TransportException; + + /** + * Process the next packet + * + * @param buffer the packet + * + * @return a boolean indicating if the processing is complete or if more packets are to be received + */ + boolean next(Message msg, SSHPacket buffer) throws TransportException; + +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java b/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java new file mode 100644 index 00000000..94b695ab --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java @@ -0,0 +1,117 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.mac; + +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; + +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; + +/** Base class for MAC implementations based on the JCE provider. */ +public class BaseMAC implements MAC { + + private final String algorithm; + private final int defbsize; + private final int bsize; + private final byte[] tmp; + private javax.crypto.Mac mac; + + public BaseMAC(String algorithm, int bsize, int defbsize) { + this.algorithm = algorithm; + this.bsize = bsize; + this.defbsize = defbsize; + tmp = new byte[defbsize]; + } + + public byte[] doFinal() { + return mac.doFinal(); + } + + public byte[] doFinal(byte[] input) { + return mac.doFinal(input); + } + + public void doFinal(byte[] buf, int offset) { + try { + if (bsize != defbsize) { + mac.doFinal(tmp, 0); + System.arraycopy(tmp, 0, buf, offset, bsize); + } else + mac.doFinal(buf, offset); + } catch (ShortBufferException e) { + throw new SSHRuntimeException(e); + } + } + + public int getBlockSize() { + return bsize; + } + + public void init(byte[] key) { + if (key.length > defbsize) { + byte[] tmp = new byte[defbsize]; + System.arraycopy(key, 0, tmp, 0, defbsize); + key = tmp; + } + + SecretKeySpec skey = new SecretKeySpec(key, algorithm); + try { + mac = SecurityUtils.getMAC(algorithm); + mac.init(skey); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + } + + public void update(byte foo[], int s, int l) { + mac.update(foo, s, l); + } + + public void update(byte[] foo) { + mac.update(foo, 0, foo.length); + } + + public void update(long i) { + tmp[0] = (byte) (i >>> 24); + tmp[1] = (byte) (i >>> 16); + tmp[2] = (byte) (i >>> 8); + tmp[3] = (byte) i; + update(tmp, 0, 4); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java new file mode 100644 index 00000000..cae47575 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.mac; + +/** HMAC-MD5 MAC. */ +public class HMACMD5 extends BaseMAC { + + /** Named factory for the HMACMD5 MAC */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public MAC create() { + return new HMACMD5(); + } + + public String getName() { + return "hmac-md5"; + } + } + + public HMACMD5() { + super("HmacMD5", 16, 16); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java new file mode 100644 index 00000000..0a726645 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.mac; + +/** HMAC-MD5-96 MAC */ +public class HMACMD596 extends BaseMAC { + + /** Named factory for the HMAC-MD5-96 MAC */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public MAC create() { + return new HMACMD596(); + } + + public String getName() { + return "hmac-md5-96"; + } + } + + public HMACMD596() { + super("HmacMD5", 12, 16); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java new file mode 100644 index 00000000..a29abb6e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.mac; + +/** HMAC-SHA1 MAC */ +public class HMACSHA1 extends BaseMAC { + + /** Named factory for the HMAC-SHA1 MAC */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public MAC create() { + return new HMACSHA1(); + } + + public String getName() { + return "hmac-sha1"; + } + } + + public HMACSHA1() { + super("HmacSHA1", 20, 20); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java new file mode 100644 index 00000000..72d1dcd9 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.mac; + +/** HMAC-SHA1-96 MAC */ +public class HMACSHA196 extends BaseMAC { + + /** Named factory for the HMAC-SHA1-96 MAC */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public MAC create() { + return new HMACSHA196(); + } + + public String getName() { + return "hmac-sha1-96"; + } + } + + public HMACSHA196() { + super("HmacSHA1", 12, 20); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/MAC.java b/src/main/java/net/schmizz/sshj/transport/mac/MAC.java new file mode 100644 index 00000000..30e8c605 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/MAC.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.mac; + +/** Message Authentication Code for use in SSH. It usually wraps a javax.crypto.Mac class. */ +public interface MAC { + + byte[] doFinal(); + + byte[] doFinal(byte[] input); + + void doFinal(byte[] buf, int offset); + + int getBlockSize(); + + void init(byte[] key); + + void update(byte[] foo); + + void update(byte[] foo, int start, int len); + + void update(long foo); +} diff --git a/src/main/java/net/schmizz/sshj/transport/random/BouncyCastleRandom.java b/src/main/java/net/schmizz/sshj/transport/random/BouncyCastleRandom.java new file mode 100644 index 00000000..e41bcc6c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/random/BouncyCastleRandom.java @@ -0,0 +1,70 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.random; + +import org.bouncycastle.crypto.prng.RandomGenerator; +import org.bouncycastle.crypto.prng.VMPCRandomGenerator; + +import java.security.SecureRandom; + +/** + * BouncyCastle Random. This pseudo random number generator uses the a very fast PRNG from BouncyCastle. + * The JRE random will be used when creating a new generator to add some random data to the seed. + */ +public class BouncyCastleRandom implements Random { + + /** Named factory for the BouncyCastle Random */ + public static class Factory implements net.schmizz.sshj.common.Factory { + + public Random create() { + return new BouncyCastleRandom(); + } + + } + + private final RandomGenerator random; + + public BouncyCastleRandom() { + random = new VMPCRandomGenerator(); + byte[] seed = new SecureRandom().generateSeed(8); + random.addSeedMaterial(seed); + } + + public void fill(byte[] bytes, int start, int len) { + random.nextBytes(bytes, start, len); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java b/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java new file mode 100644 index 00000000..44327245 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.random; + +import java.security.SecureRandom; + +/** A {@link Random} implementation using the built-in {@link SecureRandom} PRNG. */ +public class JCERandom implements Random { + + /** Named factory for the JCE {@link Random} */ + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public Random create() { + return new JCERandom(); + } + + public String getName() { + return "default"; + } + + } + + private byte[] tmp = new byte[16]; + private SecureRandom random = null; + + public JCERandom() { + random = new SecureRandom(); + } + + /** + * Fill the given byte-array with random bytes from this PRNG. + * + * @param foo the byte-array + * @param start the offset to start at + * @param len the number of bytes to fill + */ + public synchronized void fill(byte[] foo, int start, int len) { + if (start == 0 && len == foo.length) + random.nextBytes(foo); + else + synchronized (this) { + if (len > tmp.length) + tmp = new byte[len]; + random.nextBytes(tmp); + System.arraycopy(tmp, 0, foo, start, len); + } + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/random/Random.java b/src/main/java/net/schmizz/sshj/transport/random/Random.java new file mode 100644 index 00000000..5967bbd1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/random/Random.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.random; + +/** A pseudo random number generator. */ +public interface Random { + + /** + * Fill part of bytes with random values. + * + * @param bytes byte array to be filled. + * @param start index to start filling at. + * @param len length of segment to fill. + */ + void fill(byte[] bytes, int start, int len); + +} diff --git a/src/main/java/net/schmizz/sshj/transport/random/SingletonRandomFactory.java b/src/main/java/net/schmizz/sshj/transport/random/SingletonRandomFactory.java new file mode 100644 index 00000000..c7d2d210 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/random/SingletonRandomFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.random; + +import net.schmizz.sshj.common.Factory; + +/** A random factory wrapper that uses a single random instance. The underlying random instance has to be thread safe. */ +public class SingletonRandomFactory implements Random, Factory { + private final Random random; + + public SingletonRandomFactory(Factory factory) { + random = factory.create(); + } + + public Random create() { + return this; + } + + public void fill(byte[] bytes, int start, int len) { + random.fill(bytes, start, len); + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/verification/ConsoleHostKeyVerifier.java b/src/main/java/net/schmizz/sshj/transport/verification/ConsoleHostKeyVerifier.java new file mode 100644 index 00000000..5d5e15fa --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/verification/ConsoleHostKeyVerifier.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.transport.verification; + +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SecurityUtils; + +import java.io.Console; +import java.io.File; +import java.io.IOException; +import java.security.PublicKey; + +public class ConsoleHostKeyVerifier extends OpenSSHKnownHosts { + + private static final String YES = "yes"; + private static final String NO = "no"; + + private final Console console; + + public ConsoleHostKeyVerifier(File khFile, Console console) throws IOException { + super(khFile); + this.console = console; + } + + @Override + protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { + console.printf("The authenticity of host '%s' can't be established.\n" + + "%s key fingerprint is %s.\n", hostname, KeyType.fromKey(key), SecurityUtils.getFingerprint(key)); + String response = console.readLine("Are you sure you want to continue connecting (yes/no)? "); + while (!(response.equalsIgnoreCase(YES) || response.equalsIgnoreCase(NO))) { + response = console.readLine("Please explicitly enter yes/no: "); + } + if (response.equalsIgnoreCase(YES)) { + entries().add(new Entry(hostname, key)); + try { + write(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + return false; + } + + @Override + protected boolean hostKeyChangedAction(Entry entry, String hostname, PublicKey key) throws IOException { + final KeyType type = KeyType.fromKey(key); + final String fp = SecurityUtils.getFingerprint(key); + final String path = khFile.getAbsolutePath(); + console.printf( + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" + + "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n" + + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n" + + "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n" + + "Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n" + + "It is also possible that the host key has just been changed.\n" + + "The fingerprint for the %s key sent by the remote host is\n" + + "%s.\n" + + "Please contact your system administrator or" + + "add correct host key in %s to get rid of this message.\n", type, fp, path); + return false; + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/verification/HostKeyVerifier.java b/src/main/java/net/schmizz/sshj/transport/verification/HostKeyVerifier.java new file mode 100644 index 00000000..59866d54 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/verification/HostKeyVerifier.java @@ -0,0 +1,38 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.transport.verification; + +import java.security.PublicKey; + +/** Host key verification interface. */ +public interface HostKeyVerifier { + + /** + * This callback is invoked when the server's host key needs to be verified. The return value indicates to the + * caller whether the SSH connection should proceed. + *

+ * Note: host key verification is the basis for security in SSH, therefore exercise due caution in + * implementing! + * + * @param hostname remote hostname + * @param port remote port + * @param key host key of server + * + * @return {@code true} if key is acceptable, {@code false} otherwise + */ + boolean verify(String hostname, int port, PublicKey key); + +} diff --git a/src/main/java/net/schmizz/sshj/transport/verification/InsecureAccepter.java b/src/main/java/net/schmizz/sshj/transport/verification/InsecureAccepter.java new file mode 100644 index 00000000..1df74b45 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/verification/InsecureAccepter.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.transport.verification; + +import java.security.PublicKey; + +public final class InsecureAccepter implements HostKeyVerifier { + + public boolean verify(String hostname, int port, PublicKey key) { + return true; + } + +} diff --git a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java new file mode 100644 index 00000000..c848526c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java @@ -0,0 +1,267 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.transport.verification; + +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.transport.mac.HMACSHA1; +import net.schmizz.sshj.transport.mac.MAC; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// TODO: allow modifications to known_hosts e.g. adding entries + +/** + * A {@link HostKeyVerifier} implementation for a {@code known_hosts} file i.e. in the format used by OpenSSH. + *

+ * Hashed hostnames are correctly handled. + * + * @see Hashed hostnames spec + */ +public class OpenSSHKnownHosts implements HostKeyVerifier { + + private static final String LS = System.getProperty("line.separator"); + + /** Represents a single line */ + public static class Entry { + + private final MAC sha1 = new HMACSHA1(); + + private final List hosts; + private final KeyType type; + + private PublicKey key; + private String sKey; + + /** Construct an entry from the hostname and public key */ + public Entry(String host, PublicKey key) { + this.key = key; + this.hosts = Arrays.asList(host); + type = KeyType.fromKey(key); + } + + /** + * Construct an entry from a string containing the line + * + * @param line the line from a known_hosts file + * + * @throws SSHException if it could not be parsed for any reason + */ + public Entry(String line) throws SSHException { + String[] parts = line.split(" "); + if (parts.length != 3) + throw new SSHException("Line parts not 3: " + line); + hosts = Arrays.asList(parts[0].split(",")); + type = KeyType.fromString(parts[1]); + if (type == KeyType.UNKNOWN) + throw new SSHException("Unknown key type: " + parts[1]); + sKey = parts[2]; + } + + /** Checks whether this entry is applicable to some {@code hostname} */ + public boolean appliesTo(String hostname) throws IOException { + if (!hosts.isEmpty() && hosts.get(0).startsWith("|1|")) { // Hashed hostname + final String[] splitted = hosts.get(0).split("\\|"); + if (splitted.length != 4) + return false; + + final byte[] salt = Base64.decode(splitted[2]); + if (salt.length != 20) + return false; + sha1.init(salt); + + final byte[] host = Base64.decode(splitted[3]); + if (ByteArrayUtils.equals(host, sha1.doFinal(hostname.getBytes()))) + return true; + } else + // Un-hashed, possibly comma-delimited + for (String host : hosts) + if (host.equals(hostname)) + return true; + return false; + } + + /** + * Returns the public host key represented in this entry. + *

+ * The key is cached so repeated calls to this method may be made without concern. + * + * @return the host key + */ + public PublicKey getKey() { + if (key == null) { + byte[] decoded; + try { + decoded = Base64.decode(sKey); + } catch (IOException e) { + return null; + } + key = new Buffer.PlainBuffer(decoded).readPublicKey(); + } + return key; + } + + public KeyType getType() { + return type; + } + + public String getLine() { + StringBuilder line = new StringBuilder(); + for (String host : hosts) { + if (line.length() > 0) + line.append(","); + line.append(host); + } + line.append(" ").append(type.toString()); + line.append(" ").append(getKeyString()); + return line.toString(); + } + + private String getKeyString() { + if (sKey == null) { + final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key); + sKey = Base64.encodeBytes(buf.array(), buf.rpos(), buf.available()); + } + return sKey; + } + + @Override + public String toString() { + return "Entry{hostnames=" + hosts + "; type=" + type + "; key=" + getKey() + "}"; + } + + } + + private final Logger log = LoggerFactory.getLogger(getClass()); + + protected final File khFile; + protected final List entries = new ArrayList(); + + /** + * Constructs a {@code KnownHosts} object from a file location + * + * @param khFile the file location + * + * @throws IOException if there is an error reading the file + */ + public OpenSSHKnownHosts(File khFile) throws IOException { + this.khFile = khFile; + if (khFile.exists()) { + BufferedReader br = new BufferedReader(new FileReader(khFile)); + String line; + try { + // Read in the file, storing each line as an entry + while ((line = br.readLine()) != null) + try { + entries.add(new Entry(line)); + } catch (SSHException ignore) { + log.debug("Bad line ({}): {} ", ignore.toString(), line); + } + } finally { + IOUtils.closeQuietly(br); + } + } + } + + /** + * Checks whether the specified host is known per the contents of the {@code known_hosts} file. + * + * @return {@code true} on successful verification or {@code false} on failure + */ + public boolean verify(String hostname, int port, PublicKey key) { + KeyType type = KeyType.fromKey(key); + if (type == KeyType.UNKNOWN) + return false; + + if (port != 22) + hostname = "[" + hostname + "]:" + port; + + for (Entry e : entries) + try { + if (e.getType() == type && e.appliesTo(hostname)) + if (key.equals(e.getKey())) + return true; + else { + return hostKeyChangedAction(e, hostname, key); + } + } catch (IOException ioe) { + log.error("Error with {}: {}", e, ioe); + return false; + } + return hostKeyUnverifiableAction(hostname, key); + } + + protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { + return false; + } + + protected boolean hostKeyChangedAction(Entry entry, String hostname, PublicKey key) throws IOException { + log.warn("Host key for `{}` has changed!", hostname); + return false; + } + + public List entries() { + return entries; + } + + public void write() throws IOException { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile)); + for (Entry entry : entries) + bos.write((entry.getLine() + LS).getBytes()); + bos.close(); + } + + public static File detectSSHDir() { + final File sshDir = new File(System.getProperty("user.home"), ".ssh"); + return sshDir.exists() ? sshDir : null; + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/AuthParams.java b/src/main/java/net/schmizz/sshj/userauth/AuthParams.java new file mode 100644 index 00000000..38f4946b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/AuthParams.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth; + +import net.schmizz.sshj.transport.Transport; + +/** The parameters available to authentication method */ +public interface AuthParams { + + /** All userauth requests need to include the name of the next service being requested */ + String getNextServiceName(); + + /** + * Retrieve the transport which will allow sending packets; retrieving information like the session-id, remote + * host/port etc. which is needed by some method. + */ + Transport getTransport(); + + /** All userauth requests need to include the username */ + String getUsername(); + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/userauth/UserAuth.java b/src/main/java/net/schmizz/sshj/userauth/UserAuth.java new file mode 100644 index 00000000..321e9f28 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/UserAuth.java @@ -0,0 +1,99 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth; + +import net.schmizz.sshj.Service; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.method.AuthMethod; + +import java.util.Deque; + +/** + * User authentication API + * + * @see rfc4252 + */ +public interface UserAuth { + + /** + * Attempt to authenticate {@code username} using each of {@link methods} in order. {@code nextService} is the + * {@link net.schmizz.sshj.Service} that will be enabled on successful authentication. + *

+ * Authentication fails if there are no method available, i.e. if all the method failed or there were method + * available but could not be attempted because the server did not allow them. In this case, a {@code + * UserAuthException} is thrown with its cause as the last authentication failure. Other {@code UserAuthException}'s + * which may have been ignored may be accessed via {@link #getSavedExceptions()}. + *

+ * Further attempts may also be made by catching {@code UserAuthException} and retrying with this method. + * + * @param username the user to authenticate + * @param nextService the service to set on successful authentication + * @param methods the {@link AuthMethod}'s to try + * + * @throws UserAuthException in case of authentication failure + * @throws TransportException if there was a transport-layer error + */ + void authenticate(String username, Service nextService, Iterable methods) throws UserAuthException, + TransportException; + + /** + * Returns the authentication banner (if any). In some cases this is available even before the first authentication + * request has been made. + * + * @return the banner, or {@code null} if none was received + */ + String getBanner(); + + /** Returns saved exceptions that might have been ignored because there were more authentication method available. */ + Deque getSavedExceptions(); + + /** Returns the {@code timeout} for a method to successfully authenticate before it is abandoned. */ + int getTimeout(); + + /** + * Returns whether authentication was partially successful. Some server's may be configured to require multiple + * authentications; and this value will be {@code true} if at least one of the method supplied succeeded. + */ + boolean hadPartialSuccess(); + + /** + * Set the {@code timeout} for any method to successfully authenticate before it is abandoned. + * + * @param timeout the timeout in seconds + */ + void setTimeout(int timeout); + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/UserAuthException.java b/src/main/java/net/schmizz/sshj/userauth/UserAuthException.java new file mode 100644 index 00000000..50926bc9 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/UserAuthException.java @@ -0,0 +1,88 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth; + +import net.schmizz.concurrent.ExceptionChainer; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.SSHException; + +/** User authentication exception */ +public class UserAuthException extends SSHException { + + public static final ExceptionChainer chainer = new ExceptionChainer() { + + public UserAuthException chain(Throwable t) { + if (t instanceof UserAuthException) + return (UserAuthException) t; + else + return new UserAuthException(t); + } + + }; + + public UserAuthException() { + super(); + } + + public UserAuthException(DisconnectReason code) { + super(code); + } + + public UserAuthException(DisconnectReason code, String message) { + super(code, message); + } + + public UserAuthException(DisconnectReason code, String message, Throwable cause) { + super(code, message, cause); + } + + public UserAuthException(DisconnectReason code, Throwable cause) { + super(code, cause); + } + + public UserAuthException(String message) { + super(message); + } + + public UserAuthException(String message, Throwable cause) { + super(message, cause); + } + + public UserAuthException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/UserAuthProtocol.java b/src/main/java/net/schmizz/sshj/userauth/UserAuthProtocol.java new file mode 100644 index 00000000..46cc03af --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/UserAuthProtocol.java @@ -0,0 +1,250 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth; + +import net.schmizz.concurrent.Event; +import net.schmizz.sshj.AbstractService; +import net.schmizz.sshj.Service; +import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.Transport; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.method.AuthMethod; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** {@link UserAuth} implementation. */ +public class UserAuthProtocol extends AbstractService implements UserAuth, AuthParams { + + private final Set allowed = new HashSet(); + + private final Deque savedEx = new ArrayDeque(); + + private final Event result = new Event("userauth result", + UserAuthException.chainer); + + private String username; + private AuthMethod currentMethod; + private Service nextService; + + private boolean firstAttempt = true; + + private volatile String banner; + private volatile boolean partialSuccess; + + public UserAuthProtocol(Transport trans) { + super("ssh-userauth", trans); + } + + // synchronized for mutual exclusion; ensure one authenticate() ever in progress + + public synchronized void authenticate(String username, Service nextService, Iterable methods) + throws UserAuthException, TransportException { + clearState(); + + this.username = username; + this.nextService = nextService; + + // Request "ssh-userauth" service (if not already active) + request(); + + if (firstAttempt) { // Assume all allowed + for (AuthMethod meth : methods) + allowed.add(meth.getName()); + firstAttempt = false; + } + + try { + + for (AuthMethod meth : methods) + + if (allowed.contains(meth.getName())) { + + log.info("Trying `{}` auth...", meth.getName()); + + boolean success = false; + try { + success = tryWith(meth); + } catch (UserAuthException e) { + // Give other method a shot + saveException(e); + } + + if (success) { + log.info("`{}` auth successful", meth.getName()); + return; + } else + log.info("`{}` auth failed", meth.getName()); + + } else + saveException(currentMethod.getName() + " auth not allowed by server"); + + } finally { + currentMethod = null; + } + + log.debug("Had {} saved exception(s)", savedEx.size()); + throw new UserAuthException("Exhausted available authentication methods", savedEx.peek()); + } + + public String getBanner() { + return banner; + } + + public String getNextServiceName() { + return nextService.getName(); + } + + public Transport getTransport() { + return trans; + } + + /** + * Returns the exceptions that occured during authentication process but were ignored because more method were + * available for trying. + * + * @return deque of saved exceptions + */ + public Deque getSavedExceptions() { + return savedEx; + } + + public String getUsername() { + return username; + } + + public boolean hadPartialSuccess() { + return partialSuccess; + } + + @Override + public void handle(Message msg, SSHPacket buf) throws SSHException { + if (!msg.in(50, 80)) // ssh-userauth packets have message numbers between 50-80 + throw new TransportException(DisconnectReason.PROTOCOL_ERROR); + + switch (msg) { + + case USERAUTH_BANNER: + gotBanner(buf); + break; + + case USERAUTH_SUCCESS: + gotSuccess(); + break; + + case USERAUTH_FAILURE: + gotFailure(buf); + break; + + default: + gotUnknown(msg, buf); + + } + } + + @Override + public void notifyError(SSHException error) { + super.notifyError(error); + result.error(error); + } + + private void clearState() { + allowed.clear(); + savedEx.clear(); + banner = null; + } + + private void gotBanner(SSHPacket buf) { + banner = buf.readString(); + } + + private void gotFailure(SSHPacket buf) throws UserAuthException, TransportException { + allowed.clear(); + allowed.addAll(Arrays.asList(buf.readString().split(","))); + partialSuccess |= buf.readBoolean(); + if (allowed.contains(currentMethod.getName()) && currentMethod.shouldRetry()) + currentMethod.request(); + else { + saveException(currentMethod.getName() + " auth failed"); + result.set(false); + } + } + + private void gotSuccess() { + trans.setAuthenticated(); // So it can put delayed compression into force if applicable + trans.setService(nextService); // We aren't in charge anymore, next service is + result.set(true); + } + + private void gotUnknown(Message msg, SSHPacket buf) throws SSHException { + if (currentMethod == null || result == null) { + trans.sendUnimplemented(); + return; + } + + log.debug("Asking {} method to handle {} packet", currentMethod.getName(), msg); + try { + currentMethod.handle(msg, buf); + } catch (UserAuthException e) { + result.error(e); + } + } + + private void saveException(String msg) { + saveException(new UserAuthException(msg)); + } + + private void saveException(UserAuthException e) { + log.error("Saving for later - {}", e.toString()); + savedEx.push(e); + } + + private boolean tryWith(AuthMethod meth) throws UserAuthException, TransportException { + currentMethod = meth; + result.clear(); + meth.init(this); + meth.request(); + return result.get(timeout, TimeUnit.SECONDS); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java new file mode 100644 index 00000000..c9e76308 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/FileKeyProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.keyprovider; + +import net.schmizz.sshj.userauth.password.PasswordFinder; + +import java.io.File; + +/** A file key provider is initialized with a location of */ +public interface FileKeyProvider extends KeyProvider { + + enum Format { + PKCS8, OpenSSH, Unknown + } + + void init(File location); + + void init(File location, PasswordFinder pwdf); + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyPairWrapper.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyPairWrapper.java new file mode 100644 index 00000000..40018973 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyPairWrapper.java @@ -0,0 +1,71 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.keyprovider; + +import net.schmizz.sshj.common.KeyType; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** A {@link KeyProvider} wrapper around {@link java.security.KeyPair} */ +public class KeyPairWrapper implements KeyProvider { + + private final KeyPair kp; + private final KeyType type; + + public KeyPairWrapper(KeyPair kp) { + this.kp = kp; + type = KeyType.fromKey(kp.getPublic()); + } + + public KeyPairWrapper(PublicKey publicKey, PrivateKey privateKey) { + this(new KeyPair(publicKey, privateKey)); + } + + public PrivateKey getPrivate() { + return kp.getPrivate(); + } + + public PublicKey getPublic() { + return kp.getPublic(); + } + + public KeyType getType() { + return type; + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProvider.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProvider.java new file mode 100644 index 00000000..10533acb --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.keyprovider; + +import net.schmizz.sshj.common.KeyType; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** A KeyProvider is a container for a public-private keypair. */ +public interface KeyProvider { + + /** Returns the private key. */ + PrivateKey getPrivate() throws IOException; + + /** Returns the public key. */ + PublicKey getPublic() throws IOException; + + /** Returns the {@link KeyType}. */ + KeyType getType() throws IOException; + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java new file mode 100644 index 00000000..de3e2639 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.userauth.keyprovider; + +import net.schmizz.sshj.common.IOUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public class KeyProviderUtil { + + /** + * Attempts to detect how a key file is encoded. + *

+ * Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package. + * + * @param location + * + * @return name of the key file format + * + * @throws java.io.IOException + */ + public static FileKeyProvider.Format detectKeyFileFormat(File location) throws IOException { + BufferedReader br = new BufferedReader(new FileReader(location)); + String firstLine = br.readLine(); + IOUtils.closeQuietly(br); + if (firstLine == null) + throw new IOException("Empty file"); + if (firstLine.startsWith("-----BEGIN") && firstLine.endsWith("PRIVATE KEY-----")) + if (new File(location + ".pub").exists()) + // Can delay asking for password since have unencrypted pubkey + return FileKeyProvider.Format.OpenSSH; + else + // More general + return FileKeyProvider.Format.PKCS8; + /* + * TODO: Tectia, PuTTY (.ppk) ... + */ + return FileKeyProvider.Format.OpenSSH; + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java new file mode 100644 index 00000000..2fb47113 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java @@ -0,0 +1,96 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.keyprovider; + +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.KeyType; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.security.PublicKey; + +/** + * Represents an OpenSSH identity that consists of a PKCS8-encoded private key file and an unencrypted public key file + * of the same name with the {@code ".pub"} extension. This allows to delay requesting of the passphrase until the + * private key is requested. + * + * @see PKCS8KeyFile + */ +public class OpenSSHKeyFile extends PKCS8KeyFile { + + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + + public FileKeyProvider create() { + return new OpenSSHKeyFile(); + } + + public String getName() { + return "OpenSSH"; + } + } + + private PublicKey pubKey; + + @Override + public PublicKey getPublic() throws IOException { + return pubKey != null ? pubKey : super.getPublic(); + } + + @Override + public void init(File location) { + File f = new File(location + ".pub"); + if (f.exists()) + try { + BufferedReader br = new BufferedReader(new FileReader(f)); + String keydata = br.readLine(); + if (keydata != null) { + String[] parts = keydata.split(" "); + assert parts.length >= 2; + type = KeyType.fromString(parts[0]); + pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey(); + } + br.close(); + } catch (IOException e) { + // let super provide both public & private key + log.warn("Error reading public key file: {}", e.toString()); + } + super.init(location); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java new file mode 100644 index 00000000..e93bd671 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java @@ -0,0 +1,148 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.keyprovider; + +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.PasswordUtils; +import net.schmizz.sshj.userauth.password.PrivateKeyFileResource; +import net.schmizz.sshj.userauth.password.Resource; +import org.bouncycastle.openssl.EncryptionException; +import org.bouncycastle.openssl.PEMReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** Represents a PKCS8-encoded key file. This is the format used by OpenSSH and OpenSSL. */ +public class PKCS8KeyFile implements FileKeyProvider { + + public static class Factory implements net.schmizz.sshj.common.Factory.Named { + public FileKeyProvider create() { + return new PKCS8KeyFile(); + } + + public String getName() { + return "PKCS8"; + } + } + + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected PasswordFinder pwdf; + protected File location; + protected Resource resource; + protected KeyPair kp; + + protected KeyType type; + + protected char[] passphrase; // for blanking out + + public PrivateKey getPrivate() throws IOException { + return kp != null ? kp.getPrivate() : (kp = readKeyPair()).getPrivate(); + } + + public PublicKey getPublic() throws IOException { + return kp != null ? kp.getPublic() : (kp = readKeyPair()).getPublic(); + } + + public KeyType getType() throws IOException { + return type != null ? type : (type = KeyType.fromKey(getPublic())); + } + + public void init(File location) { + assert location != null; + this.location = location; + resource = new PrivateKeyFileResource(location.getAbsolutePath()); + } + + public void init(File location, PasswordFinder pwdf) { + init(location); + this.pwdf = pwdf; + } + + protected org.bouncycastle.openssl.PasswordFinder makeBouncyPasswordFinder() { + if (pwdf == null) + return null; + else + return new org.bouncycastle.openssl.PasswordFinder() { + public char[] getPassword() { + return passphrase = pwdf.reqPassword(resource); + } + }; + } + + protected KeyPair readKeyPair() throws IOException { + KeyPair kp = null; + org.bouncycastle.openssl.PasswordFinder pFinder = makeBouncyPasswordFinder(); + PEMReader r = null; + Object o = null; + try { + for (; ;) { + // while the PasswordFinder tells us we should retry + try { + r = new PEMReader(new InputStreamReader(new FileInputStream(location)), pFinder); + o = r.readObject(); + } catch (EncryptionException e) { + if (pwdf.shouldRetry(resource)) + continue; + else + throw e; + } finally { + IOUtils.closeQuietly(r); + } + break; + } + } finally { + PasswordUtils.blankOut(passphrase); + } + + if (o == null) + throw new IOException("Could not read key pair from: " + location); + if (o instanceof KeyPair) + kp = (KeyPair) o; + else + log.debug("Expected KeyPair, got {}", o); + return kp; + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AbstractAuthMethod.java b/src/main/java/net/schmizz/sshj/userauth/method/AbstractAuthMethod.java new file mode 100644 index 00000000..4dd727c6 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/AbstractAuthMethod.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.AuthParams; +import net.schmizz.sshj.userauth.UserAuthException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** This abstract class for {@link AuthMethod} implements common or default functionality. */ +public abstract class AbstractAuthMethod implements AuthMethod { + + /** Logger */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private final String name; + + /** {@link net.schmizz.sshj.userauth.AuthParams} useful for building request. */ + protected AuthParams params; + + /** Create with the {@code name} of this authentication method. */ + protected AbstractAuthMethod(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void handle(Message msg, SSHPacket buf) throws UserAuthException, TransportException { + throw new UserAuthException("Unknown packet received during " + getName() + " auth: " + msg); + } + + public void init(AuthParams params) { + this.params = params; + } + + public void request() throws UserAuthException, TransportException { + params.getTransport().write(buildReq()); + } + + public boolean shouldRetry() { + return false; + } + + /** + * Builds a {@link net.schmizz.sshj.common.SSHPacket} containing the fields common to all authentication method. + * Method-specific fields can further be put into this buffer. + */ + protected SSHPacket buildReq() throws UserAuthException { + return new SSHPacket(Message.USERAUTH_REQUEST) // SSH_MSG_USERAUTH_REQUEST + .putString(params.getUsername()) // username goes first + .putString(params.getNextServiceName()) // the service that we'd like on success + .putString(name); // name of auth method + + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AuthHostbased.java b/src/main/java/net/schmizz/sshj/userauth/method/AuthHostbased.java new file mode 100644 index 00000000..5068cf65 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthHostbased.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.userauth.UserAuthException; +import net.schmizz.sshj.userauth.keyprovider.KeyProvider; + +// TODO check if this even works...! + +/** Implements the {@code hostbased} SSH authentication method. */ +public class AuthHostbased extends KeyedAuthMethod { + + protected final String hostname; + protected final String hostuser; + + public AuthHostbased(KeyProvider kProv, String hostuser) { + this(kProv, hostuser, null); + } + + public AuthHostbased(KeyProvider kProv, String hostuser, String hostname) { + super("hostbased", kProv); + assert hostuser != null; + this.hostuser = hostuser; + this.hostname = hostname; + } + + @Override + protected SSHPacket buildReq() throws UserAuthException { + SSHPacket req = putPubKey(super.buildReq()); + req.putString(hostname == null ? params.getTransport().getRemoteHost() : hostname) // + .putString(hostuser); + return putSig(req); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AuthMethod.java b/src/main/java/net/schmizz/sshj/userauth/method/AuthMethod.java new file mode 100644 index 00000000..e79f9b7b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthMethod.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +import net.schmizz.sshj.common.SSHPacketHandler; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.AuthParams; +import net.schmizz.sshj.userauth.UserAuthException; + +/** + * An authentication method of the SSH Authentication Protocol. + * + * @see net.schmizz.sshj.userauth.UserAuth + */ +public interface AuthMethod extends SSHPacketHandler { + + /** Returns assigned name of this authentication method */ + String getName(); + + /** + * Initializes this {@link AuthMethod} with the {@link net.schmizz.sshj.userauth.AuthParams parameters} needed for + * authentication. This method must be called before requesting authentication with this method. + */ + void init(AuthParams params); + + /** + * @throws net.schmizz.sshj.userauth.UserAuthException + * + * @throws TransportException + */ + void request() throws UserAuthException, TransportException; + + /** Returns whether authentication should be reattempted if it failed. */ + boolean shouldRetry(); + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AuthNone.java b/src/main/java/net/schmizz/sshj/userauth/method/AuthNone.java new file mode 100644 index 00000000..15fee28b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthNone.java @@ -0,0 +1,49 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +/** + * {@code none} auth. No authentication information is exchanged in the request packet save username and the next + * service requested. This method generally fails and is typically used to find out the method allowed by an SSH server + * (sent as part of the {@code SSH_MSG_USERAUTH_FAILURE} packet) + */ +public class AuthNone extends AbstractAuthMethod { + + AuthNone() { + super("none"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AuthPassword.java b/src/main/java/net/schmizz/sshj/userauth/method/AuthPassword.java new file mode 100644 index 00000000..11f32e1e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthPassword.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.AuthParams; +import net.schmizz.sshj.userauth.UserAuthException; +import net.schmizz.sshj.userauth.password.AccountResource; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.Resource; + +/** Implements the {@code password} authentication method. Password-change request handling is not currently supported. */ +public class AuthPassword extends AbstractAuthMethod { + + private final PasswordFinder pwdf; + private Resource resource; + + public AuthPassword(PasswordFinder pwdf) { + super("password"); + this.pwdf = pwdf; + + } + + @Override + public void init(AuthParams params) { + super.init(params); + resource = new AccountResource(params.getUsername(), params.getTransport().getRemoteHost()); + } + + @Override + public SSHPacket buildReq() throws UserAuthException { + log.info("Requesting password for " + resource); + char[] password = pwdf.reqPassword(resource); + if (password == null) + throw new UserAuthException("Was given null password for " + resource); + else + return super.buildReq() // the generic stuff + .putBoolean(false) // no, we are not responding to a CHANGEREQ + .putPassword(password); + } + + @Override + public void handle(Message cmd, SSHPacket buf) throws UserAuthException, TransportException { + if (cmd == Message.USERAUTH_60) + throw new UserAuthException("Password change request received; unsupported operation"); + else + super.handle(cmd, buf); + } + + /** + * Returns {@code true} if the associated {@link PasswordFinder} tells that we should retry with a new password that + * it will supply. + */ + @Override + public boolean shouldRetry() { + return pwdf.shouldRetry(resource); + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/userauth/method/AuthPublickey.java b/src/main/java/net/schmizz/sshj/userauth/method/AuthPublickey.java new file mode 100644 index 00000000..c4e513f4 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/AuthPublickey.java @@ -0,0 +1,105 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +import net.schmizz.sshj.common.Message; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.UserAuthException; +import net.schmizz.sshj.userauth.keyprovider.KeyProvider; + +import java.io.IOException; + +/** + * Implements the {@code "publickey"} SSH authentication method. + *

+ * Requesteing authentication with this method first sends a "feeler" request with just the public key, and if the + * server responds with {@code SSH_MSG_USERAUTH_PK_OK} indicating that the key is acceptable, it proceeds to send a + * request signed with the private key. Therefore, private keys are not requested from the associated {@link + * KeyProvider} unless needed. + */ +public class AuthPublickey extends KeyedAuthMethod { + + /** Initialize this method with the provider for public and private key. */ + public AuthPublickey(KeyProvider kProv) { + super("publickey", kProv); + } + + /** Internal use. */ + @Override + public void handle(Message cmd, SSHPacket buf) throws UserAuthException, TransportException { + if (cmd == Message.USERAUTH_60) + sendSignedReq(); + else + super.handle(cmd, buf); + } + + /** + * Builds SSH_MSG_USERAUTH_REQUEST packet. + * + * @param signed whether the request packet will contain signature + * + * @return the {@link SSHPacket} containing the request packet + * + * @throws UserAuthException + */ + private SSHPacket buildReq(boolean signed) throws UserAuthException { + try { + kProv.getPublic(); + } catch (IOException ioe) { + throw new UserAuthException("Problem getting public key", ioe); + } + return putPubKey(super.buildReq().putBoolean(signed)); + } + + /** + * Send SSH_MSG_USERAUTH_REQUEST containing the signature. + * + * @throws UserAuthException + * @throws TransportException + */ + private void sendSignedReq() throws UserAuthException, TransportException { + log.debug("Sending signed request"); + params.getTransport().write(putSig(buildReq(true))); + } + + /** Builds a feeler request (sans signature). */ + @Override + protected SSHPacket buildReq() throws UserAuthException { + return buildReq(false); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java b/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java new file mode 100644 index 00000000..4bd093e1 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java @@ -0,0 +1,94 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file may incorporate work covered by the following copyright and + * permission notice: + * + * 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 net.schmizz.sshj.userauth.method; + +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.signature.Signature; +import net.schmizz.sshj.userauth.UserAuthException; +import net.schmizz.sshj.userauth.keyprovider.KeyProvider; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; + +public abstract class KeyedAuthMethod extends AbstractAuthMethod { + protected KeyProvider kProv; + + public KeyedAuthMethod(String name, KeyProvider kProv) { + super(name); + this.kProv = kProv; + } + + protected SSHPacket putPubKey(SSHPacket reqBuf) throws UserAuthException { + PublicKey key; + try { + key = kProv.getPublic(); + } catch (IOException ioe) { + throw new UserAuthException("Problem getting public key", ioe); + } + + // public key as 2 strings: [ key type | key blob ] + reqBuf.putString(KeyType.fromKey(key).toString()) + .putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData()); + + return reqBuf; + } + + protected SSHPacket putSig(SSHPacket reqBuf) throws UserAuthException { + PrivateKey key; + try { + key = kProv.getPrivate(); + } catch (IOException ioe) { + throw new UserAuthException("Problem getting private key", ioe); + } + + final String kt = KeyType.fromKey(key).toString(); + Signature sigger = Factory.Named.Util.create(params.getTransport().getConfig().getSignatureFactories(), kt); + if (sigger == null) + throw new UserAuthException("Could not create signature instance for " + kt + " key"); + + sigger.init(null, key); + sigger.update(new Buffer.PlainBuffer().putString(params.getTransport().getSessionID()) // + .putBuffer(reqBuf) // & rest of the data for sig + .getCompactData()); + reqBuf.putSignature(kt, sigger.sign()); + return reqBuf; + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java b/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java new file mode 100644 index 00000000..cd828907 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/password/AccountResource.java @@ -0,0 +1,24 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.userauth.password; + +public class AccountResource extends Resource { + + public AccountResource(String user, String host) { + super(user + "@" + host); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PasswordFinder.java b/src/main/java/net/schmizz/sshj/userauth/password/PasswordFinder.java new file mode 100644 index 00000000..c4db7fab --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/password/PasswordFinder.java @@ -0,0 +1,45 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.userauth.password; + +/** An interface for servicing requests for plaintext passwords. */ +public interface PasswordFinder { + + /** + * Request password for specified resource. + *

+ * This method may return {@code null} when the request cannot be serviced, e.g. when the user cancels a password + * prompt. The consequences of returning {@code null} are specific to the requestor. + * + * @param resource the resource for which password is being requested + * + * @return the password or {@code null} + */ + char[] reqPassword(Resource resource); + + /** + * If password turns out to be incorrect, indicates whether another call to {@link #reqPassword(Resource)} should be + * made. + *

+ * This method is geared at interactive implementations, and stub implementations may simply return {@code false}. + * + * @param resource + * + * @return whether to retry requesting password for a particular resource + */ + boolean shouldRetry(Resource resource); + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PasswordUtils.java b/src/main/java/net/schmizz/sshj/userauth/password/PasswordUtils.java new file mode 100644 index 00000000..f8ffbad0 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/password/PasswordUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.userauth.password; + +import java.util.Arrays; +import java.util.Map; + +/** Static utility method and factories */ +public class PasswordUtils { + + /** + * Blank out a character array + * + * @param pwd the character array + */ + public static void blankOut(char[] pwd) { + if (pwd != null) + Arrays.fill(pwd, ' '); + } + + /** + * @param password the password as a char[] + * + * @return the constructed {@link net.schmizz.sshj.userauth.password.PasswordFinder} + */ + public static PasswordFinder createOneOff(final char[] password) { + if (password == null) + return null; + else + return new PasswordFinder() { + public char[] reqPassword(Resource resource) { + char[] cloned = password.clone(); + blankOut(password); + return cloned; + } + + public boolean shouldRetry(Resource resource) { + return false; + } + }; + } + + public static PasswordFinder createResourceBased(final Map, String> passwordMap) { + return new PasswordFinder() { + public char[] reqPassword(Resource resource) { + return passwordMap.get(resource).toCharArray(); + } + + public boolean shouldRetry(Resource resource) { + return false; + } + + }; + } +} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java new file mode 100644 index 00000000..c34cd340 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java @@ -0,0 +1,24 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.userauth.password; + +public class PrivateKeyFileResource extends Resource { + + public PrivateKeyFileResource(String path) { + super(path); + } + +} diff --git a/src/main/java/net/schmizz/sshj/userauth/password/Resource.java b/src/main/java/net/schmizz/sshj/userauth/password/Resource.java new file mode 100644 index 00000000..a8e00436 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/userauth/password/Resource.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.userauth.password; + +/** A password-protected resource */ +public abstract class Resource { + + private final H detail; + + public Resource(H handle) { + this.detail = handle; + } + + public H getDetail() { + return detail; + } + + @Override + public String toString() { + return "[" + getClass().getSimpleName() + "] " + detail; + } + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java b/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java new file mode 100644 index 00000000..75c8fd52 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractFileTransfer { + + /** Logger */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + public static final ModeGetter defaultModeGetter = new DefaultModeGetter(); + public static final ModeSetter defaultModeSetter = new DefaultModeSetter(); + + private volatile ModeGetter modeGetter = defaultModeGetter; + private volatile ModeSetter modeSetter = defaultModeSetter; + + public void setModeGetter(ModeGetter modeGetter) { + this.modeGetter = (modeGetter == null) ? defaultModeGetter : modeGetter; + } + + public ModeGetter getModeGetter() { + return this.modeGetter; + } + + public void setModeSetter(ModeSetter modeSetter) { + this.modeSetter = (modeSetter == null) ? defaultModeSetter : modeSetter; + } + + public ModeSetter getModeSetter() { + return this.modeSetter; + } + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/DefaultModeGetter.java b/src/main/java/net/schmizz/sshj/xfer/DefaultModeGetter.java new file mode 100644 index 00000000..ed124a73 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/DefaultModeGetter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.io.File; +import java.io.IOException; + +/** + * Default implementation of {@link ModeGetter} that supplies file permissions as {@code "0644"}, directory permissions + * as {@code "0755"}, and does not supply mtime and atime. + */ +public class DefaultModeGetter implements ModeGetter { + + public long getLastAccessTime(File f) { + return 0; + } + + public long getLastModifiedTime(File f) { + // return f.lastModified() / 1000; + return 0; + } + + public int getPermissions(File f) throws IOException { + if (f.isDirectory()) + return 0755; + else if (f.isFile()) + return 0644; + else + throw new IOException("Unsupported file type: " + f); + } + + public boolean preservesTimes() { + return false; + } + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/DefaultModeSetter.java b/src/main/java/net/schmizz/sshj/xfer/DefaultModeSetter.java new file mode 100644 index 00000000..995791d6 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/DefaultModeSetter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.io.File; +import java.io.IOException; + + +/** Default implementation of {@link ModeSetter} that does not set any permissions or preserve mtime and atime. */ +public class DefaultModeSetter implements ModeSetter { + + public void setLastAccessedTime(File f, long t) throws IOException { + // can't do ntn + } + + public void setLastModifiedTime(File f, long t) throws IOException { + // f.setLastModified(t * 1000); + } + + public void setPermissions(File f, int perms) throws IOException { + // TODO: set user's rwx permissions; can't do anything about group and world + } + + public boolean preservesTimes() { + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/xfer/FilePermission.java b/src/main/java/net/schmizz/sshj/xfer/FilePermission.java new file mode 100644 index 00000000..8450a737 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/FilePermission.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public enum FilePermission { + + /** read permission, owner */ + USR_R(00400), + /** write permission, owner */ + USR_W(00200), + /** execute/search permission, owner */ + USR_X(00100), + /** read permission, group */ + GRP_R(00040), + /** write permission, group */ + GRP_W(00020), + /** execute/search permission, group */ + GRP_X(00010), + /** read permission, others */ + OTH_R(00004), + /** write permission, others */ + OTH_W(00002), + /** execute/search permission, group */ + OTH_X(00001), + /** set-user-ID on execution */ + SUID(04000), + /** set-group-ID on execution */ + SGID(02000), + /** on directories, restricted deletion flag */ + STICKY(01000), + // Composite: + /** read, write, execute/search by user */ + USR_RWX(USR_R, USR_W, USR_X), + /** read, write, execute/search by group */ + GRP_RWX(GRP_R, GRP_W, GRP_X), + /** read, write, execute/search by other */ + OTH_RWX(OTH_R, OTH_W, OTH_X); + + private final int val; + + private FilePermission(int val) { + this.val = val; + } + + private FilePermission(FilePermission... perms) { + int val = 0; + for (FilePermission perm : perms) + val |= perm.val; + this.val = val; + } + + public static Set fromMask(int mask) { + List perms = new LinkedList(); + for (FilePermission p : FilePermission.values()) + if ((mask & p.val) == p.val) + perms.add(p); + return new HashSet(perms); + } + + public static int toMask(Set perms) { + int mask = 0; + for (FilePermission p : perms) + mask |= p.val; + return mask; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java b/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java new file mode 100644 index 00000000..0e4a69da --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/FileTransfer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.io.IOException; + +public interface FileTransfer { + + void upload(String localPath, String remotePath) throws IOException; + + void download(String remotePath, String localPath) throws IOException; + + ModeGetter getModeGetter(); + + void setModeGetter(ModeGetter modeGetter); + + ModeSetter getModeSetter(); + + void setModeSetter(ModeSetter modeSetter); + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/FileTransferUtil.java b/src/main/java/net/schmizz/sshj/xfer/FileTransferUtil.java new file mode 100644 index 00000000..20cedb3c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/FileTransferUtil.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.io.File; +import java.io.IOException; + +public class FileTransferUtil { + + public static File getTargetDirectory(File f, String dirname) throws IOException { + if (f.exists()) + if (f.isDirectory()) { + if (!f.getName().equals(dirname)) + f = new File(f, dirname); + } else + throw new IOException(f + " - already exists as a file; directory required"); + + if (!f.exists() && !f.mkdir()) + throw new IOException("Failed to create directory: " + f); + + return f; + } + + public static File getTargetFile(File f, String filename) throws IOException { + if (f.isDirectory()) + f = new File(f, filename); + + if (!f.exists()) { + if (!f.createNewFile()) + throw new IOException("Could not create: " + f); + } else if (f.isDirectory()) + throw new IOException("A directory by the same name already exists: " + f); + + return f; + } + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/ModeGetter.java b/src/main/java/net/schmizz/sshj/xfer/ModeGetter.java new file mode 100644 index 00000000..d422fd63 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/ModeGetter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.io.File; +import java.io.IOException; + +/** An interface for retrieving information about file permissions and times. */ +public interface ModeGetter { + + /** + * Returns last access time for {@code f}. + * + * @param f the file + * + * @return time in seconds since Unix epoch + * + * @throws IOException + */ + long getLastAccessTime(File f) throws IOException; + + /** + * Returns last modified time for {@code f}. + * + * @param f the file + * + * @return time in seconds since Unix epoch + * + * @throws IOException + */ + long getLastModifiedTime(File f) throws IOException; + + /** + * Returns the permissions for {@code f}. + * + * @param f the file + * + * @return permissions in octal format, e.g. 0644 + * + * @throws IOException + */ + int getPermissions(File f) throws IOException; + + /** Whether this implementation can provide mtime and atime information. */ + boolean preservesTimes(); + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/xfer/ModeSetter.java b/src/main/java/net/schmizz/sshj/xfer/ModeSetter.java new file mode 100644 index 00000000..b4a7d406 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/ModeSetter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer; + +import java.io.File; +import java.io.IOException; + +/** An interface for setting file permissions and times. */ +public interface ModeSetter { + + /** + * Set the last access time for {@code f}. + * + * @param f the file + * @param t time in seconds since Unix epoch + * + * @throws IOException + */ + void setLastAccessedTime(File f, long t) throws IOException; + + /** + * Set the last modified time for {@code f}. + * + * @param f the file + * @param t time in seconds since Unix epoch + * + * @throws IOException + */ + void setLastModifiedTime(File f, long t) throws IOException; + + /** + * Set the permissions for {@code f}. + * + * @param f the file + * @param perms permissions in octal format, e.g. "644" + * + * @throws IOException + */ + void setPermissions(File f, int perms) throws IOException; + + /** Whether this implementation is interested in preserving mtime and atime. */ + boolean preservesTimes(); + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java new file mode 100644 index 00000000..426dabbb --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java @@ -0,0 +1,188 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer.scp; + +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import net.schmizz.sshj.xfer.FileTransferUtil; +import net.schmizz.sshj.xfer.ModeSetter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +/** Support for uploading files over a connected {@link net.schmizz.sshj.SSHClient} link using SCP. */ +public final class SCPDownloadClient extends SCPEngine { + + private final ModeSetter modeSetter; + + private boolean recursive = true; + + SCPDownloadClient(SessionFactory host, ModeSetter modeSetter) { + super(host); + this.modeSetter = modeSetter; + } + + /** Download a file from {@code sourcePath} on the connected host to {@code targetPath} locally. */ + @Override + public synchronized int copy(String sourcePath, String targetPath) throws IOException { + return super.copy(sourcePath, targetPath); + } + + public boolean getRecursive() { + return recursive; + } + + public void setRecursive(boolean recursive) { + this.recursive = recursive; + } + + @Override + void startCopy(String sourcePath, String targetPath) throws IOException { + init(sourcePath); + + signal("Start status OK"); + + String msg = readMessage(true); + do + process(null, msg, new File(targetPath)); + while ((msg = readMessage(false)) != null); + } + + private void init(String source) throws SSHException { + List args = new LinkedList(); + args.add(Arg.SOURCE); + args.add(Arg.QUIET); + if (recursive) + args.add(Arg.RECURSIVE); + if (modeSetter.preservesTimes()) + args.add(Arg.PRESERVE_TIMES); + execSCPWith(args, source); + } + + private long parseLong(String longString, String valType) throws SCPException { + try { + return Long.parseLong(longString); + } catch (NumberFormatException nfe) { + throw new SCPException("Could not parse " + valType + " from `" + longString + "`", nfe); + } + } + + /* e.g. "C0644" -> 0644; "D0755" -> 0755 */ + + private int parsePermissions(String cmd) throws SCPException { + if (cmd.length() != 5) + throw new SCPException("Could not parse permissions from `" + cmd + "`"); + return Integer.parseInt(cmd.substring(1), 8); + } + + private void prepare(File f, int perms, String tMsg) throws IOException { + modeSetter.setPermissions(f, perms); + + if (tMsg != null && modeSetter.preservesTimes()) { + String[] tMsgParts = tokenize(tMsg, 4); // e.g. T 0 0 + modeSetter.setLastModifiedTime(f, parseLong(tMsgParts[0].substring(1), "last modified time")); + modeSetter.setLastAccessedTime(f, parseLong(tMsgParts[2], "last access time")); + } + } + + private boolean process(String bufferedTMsg, String msg, File f) throws IOException { + if (msg.length() < 1) + throw new SCPException("Could not parse message `" + msg + "`"); + + switch (msg.charAt(0)) { + + case 'T': + signal("ACK: T"); + process(msg, readMessage(true), f); + break; + + case 'C': + processFile(msg, bufferedTMsg, f); + break; + + case 'D': + processDirectory(msg, bufferedTMsg, f); + break; + + case 'E': + return true; + + case (char) 1: + addWarning(msg.substring(1)); + break; + + case (char) 2: + throw new SCPException("Remote SCP command returned error: " + msg.substring(1)); + + default: + String err = "Unrecognized message: `" + msg + "`"; + sendMessage((char) 2 + err); + throw new SCPException(err); + } + + return false; + } + + private void processDirectory(String dMsg, String tMsg, File f) throws IOException { + String[] dMsgParts = tokenize(dMsg, 3); // e.g. D0755 0 + + long length = parseLong(dMsgParts[1], "dir length"); + if (length != 0) + throw new IOException("Remote SCP command sent strange directory length: " + length); + + f = FileTransferUtil.getTargetDirectory(f, dMsgParts[2]); + prepare(f, parsePermissions(dMsgParts[0]), tMsg); + + signal("ACK: D"); + + do { + ; + } while (!process(null, readMessage(), f)); + + signal("ACK: E"); + } + + private void processFile(String cMsg, String tMsg, File f) throws IOException { + String[] cMsgParts = tokenize(cMsg, 3); + + long length = parseLong(cMsgParts[1], "length"); + + f = FileTransferUtil.getTargetFile(f, cMsgParts[2]); + prepare(f, parsePermissions(cMsgParts[0]), tMsg); + + signal("Remote can start transfer"); + final FileOutputStream fos = new FileOutputStream(f); + try { + transfer(scp.getInputStream(), fos, scp.getLocalMaxPacketSize(), length); + } finally { + IOUtils.closeQuietly(fos); + } + check("Remote agrees transfer done"); + signal("Transfer done"); + } + + private String[] tokenize(String msg, int numPartsExpected) throws IOException { + String[] parts = msg.split(" "); + if (parts.length != numPartsExpected) + throw new IOException("Could not parse message received from remote SCP: " + msg); + return parts; + } + +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java new file mode 100644 index 00000000..92fcb9dc --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPEngine.java @@ -0,0 +1,201 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer.scp; + +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.connection.channel.direct.Session.Command; +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** @see SCP Protocol */ +abstract class SCPEngine { + + static enum Arg { + SOURCE('f'), SINK('t'), RECURSIVE('r'), VERBOSE('v'), PRESERVE_TIMES('p'), QUIET('q'); + + private final char a; + + private Arg(char a) { + this.a = a; + } + + @Override + public String toString() { + return "-" + a; + } + } + + static final String SCP_COMMAND = "scp"; + + static final char LF = '\n'; + + final Logger log = LoggerFactory.getLogger(getClass()); + + final SessionFactory host; + final Queue warnings = new LinkedList(); + + Command scp; + int exitStatus; + + SCPEngine(SessionFactory host) { + this.host = host; + } + + public int copy(String sourcePath, String targetPath) throws IOException { + cleanSlate(); + try { + startCopy(sourcePath, targetPath); + } finally { + exit(); + } + return exitStatus; + } + + public int getExitStatus() { + return exitStatus; + } + + public Queue getWarnings() { + return warnings; + } + + public boolean hadWarnings() { + return !warnings.isEmpty(); + } + + void addWarning(String warning) { + log.warn(warning); + warnings.add(warning); + } + + void check(String what) throws IOException { + int code = scp.getInputStream().read(); + switch (code) { + case -1: + String stderr = scp.getErrorAsString(); + if (!stderr.isEmpty()) + stderr = ". Additional info: `" + stderr + "`"; + throw new SCPException("EOF while expecting response to protocol message" + stderr); + case 0: // OK + log.debug(what); + return; + case 1: + addWarning(readMessage()); + break; + case 2: + throw new SCPException("Remote SCP command had error: " + readMessage()); + default: + throw new SCPException("Received unknown response code"); + } + } + + void cleanSlate() { + exitStatus = -1; + warnings.clear(); + } + + void execSCPWith(List args, String path) throws SSHException { + StringBuilder cmd = new StringBuilder(SCP_COMMAND); + for (Arg arg : args) + cmd.append(" ").append(arg); + cmd.append(" ").append((path == null || path.equals("")) ? "." : path); + scp = host.startSession().exec(cmd.toString()); + } + + void exit() { + if (scp != null) { + + IOUtils.closeQuietly(scp); + + if (scp.getExitStatus() != null) { + exitStatus = scp.getExitStatus(); + if (scp.getExitStatus() != 0) + log.warn("SCP exit status: {}", scp.getExitStatus()); + } else + exitStatus = -1; + + if (scp.getExitSignal() != null) + log.warn("SCP exit signal: {}", scp.getExitSignal()); + } + + scp = null; + } + + String readMessage() throws IOException { + return readMessage(true); + } + + String readMessage(boolean errOnEOF) throws IOException { + StringBuilder sb = new StringBuilder(); + int x; + while ((x = scp.getInputStream().read()) != LF) + if (x == -1) { + if (errOnEOF) + throw new IOException("EOF while reading message"); + else + return null; + } else + sb.append((char) x); + log.debug("Read message: {}", sb); + return sb.toString(); + } + + void sendMessage(String msg) throws IOException { + log.debug("Sending message: {}", msg); + scp.getOutputStream().write((msg + LF).getBytes()); + scp.getOutputStream().flush(); + check("Message ACK received"); + } + + void signal(String what) throws IOException { + log.debug("Signalling: {}", what); + scp.getOutputStream().write(0); + scp.getOutputStream().flush(); + } + + abstract void startCopy(String sourcePath, String targetPath) throws IOException; + + void transfer(InputStream in, OutputStream out, int bufSize, long len) throws IOException { + final byte[] buf = new byte[bufSize]; + long count = 0; + int read = 0; + + final long startTime = System.currentTimeMillis(); + + while (count < len && (read = in.read(buf, 0, (int) Math.min(bufSize, len - count))) != -1) { + out.write(buf, 0, read); + count += read; + } + out.flush(); + + final double sizeKiB = count / 1024.0; + final double timeSeconds = (System.currentTimeMillis() - startTime) / 1000.0; + log.info(sizeKiB + " KiB transferred in {} seconds ({} KiB/s)", timeSeconds, (sizeKiB / timeSeconds)); + + if (read == -1) + throw new IOException("Had EOF before transfer completed"); + } + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPException.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPException.java new file mode 100644 index 00000000..93d4812b --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer.scp; + +import net.schmizz.sshj.common.SSHException; + +public class SCPException extends SSHException { + public SCPException(String message) { + super(message); + } + + public SCPException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java new file mode 100644 index 00000000..07cea3b3 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPFileTransfer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer.scp; + +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import net.schmizz.sshj.xfer.AbstractFileTransfer; +import net.schmizz.sshj.xfer.FileTransfer; + +import java.io.IOException; + +public class SCPFileTransfer extends AbstractFileTransfer implements FileTransfer { + private final SessionFactory sessionFactory; + + public SCPFileTransfer(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public SCPDownloadClient newSCPDownloadClient() { + return new SCPDownloadClient(sessionFactory, getModeSetter()); + } + + public SCPUploadClient newSCPUploadClient() { + return new SCPUploadClient(sessionFactory, getModeGetter()); + } + + public void download(String remotePath, String localPath) throws IOException { + newSCPDownloadClient().copy(remotePath, localPath); + } + + public void upload(String localPath, String remotePath) throws IOException { + newSCPUploadClient().copy(localPath, remotePath); + } + +} diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPUploadClient.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPUploadClient.java new file mode 100644 index 00000000..8deabb0e --- /dev/null +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPUploadClient.java @@ -0,0 +1,120 @@ +/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 net.schmizz.sshj.xfer.scp; + +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.connection.channel.direct.SessionFactory; +import net.schmizz.sshj.xfer.ModeGetter; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; + +/** Support for uploading files over a connected link using SCP. */ +public final class SCPUploadClient extends SCPEngine { + + private final ModeGetter modeGetter; + + private FileFilter fileFilter; + + SCPUploadClient(SessionFactory host, ModeGetter modeGetter) { + super(host); + this.modeGetter = modeGetter; + } + + /** Upload a file from {@code sourcePath} locally to {@code targetPath} on the remote host. */ + @Override + public synchronized int copy(String sourcePath, String targetPath) throws IOException { + return super.copy(sourcePath, targetPath); + } + + public void setFileFilter(FileFilter fileFilter) { + this.fileFilter = fileFilter; + } + + @Override + protected synchronized void startCopy(String sourcePath, String targetPath) throws IOException { + init(targetPath); + check("Start status OK"); + process(new File(sourcePath)); + } + + private File[] getChildren(File f) throws IOException { + File[] files = fileFilter == null ? f.listFiles() : f.listFiles(fileFilter); + if (files == null) + throw new IOException("Error listing files in directory: " + f); + return files; + } + + private void init(String target) throws SSHException { + List args = new LinkedList(); + args.add(Arg.SINK); + args.add(Arg.RECURSIVE); + if (modeGetter.preservesTimes()) + args.add(Arg.PRESERVE_TIMES); + execSCPWith(args, target); + } + + private void process(File f) throws IOException { + if (f.isDirectory()) + sendDirectory(f); + else if (f.isFile()) + sendFile(f); + else + throw new IOException(f + " is not a regular file or directory"); + } + + private void sendDirectory(File f) throws IOException { + log.info("Entering directory `{}`", f.getName()); + preserveTimeIfPossible(f); + sendMessage("D0" + getPermString(f) + " 0 " + f.getName()); + + for (File child : getChildren(f)) + process(child); + + sendMessage("E"); + log.info("Exiting directory `{}`", f.getName()); + } + + private void sendFile(File f) throws IOException { + log.info("Sending `{}`...", f.getName()); + preserveTimeIfPossible(f); + final InputStream src = new FileInputStream(f); + try { + sendMessage("C0" + getPermString(f) + " " + f.length() + " " + f.getName()); + transfer(src, scp.getOutputStream(), scp.getRemoteMaxPacketSize(), f.length()); + signal("Transfer done"); + check("Remote agrees transfer done"); + } finally { + IOUtils.closeQuietly(src); + } + } + + private void preserveTimeIfPossible(File f) throws IOException { + if (modeGetter.preservesTimes()) + sendMessage("T" + modeGetter.getLastModifiedTime(f) + " 0 " + modeGetter.getLastAccessTime(f) + " 0"); + } + + private String getPermString(File f) throws IOException { + return Integer.toOctalString(modeGetter.getPermissions(f) & 07777); + } + +} diff --git a/src/test/java/net/schmizz/sshj/SmokeTest.java b/src/test/java/net/schmizz/sshj/SmokeTest.java new file mode 100644 index 00000000..4bd06aca --- /dev/null +++ b/src/test/java/net/schmizz/sshj/SmokeTest.java @@ -0,0 +1,108 @@ +package net.schmizz.sshj;/* + * Copyright 2010 Shikhar Bhushan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.userauth.UserAuthException; +import net.schmizz.sshj.util.BogusPasswordAuthenticator; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.PatternLayout; +import org.apache.sshd.SshServer; +import org.apache.sshd.common.keyprovider.FileKeyPairProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.ServerSocket; + +import static org.junit.Assert.assertTrue; + +/* Kinda basic right now */ + +public class SmokeTest { + static { + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + } + + private SSHClient ssh; + private SshServer sshd; + + private final String hostname = "localhost"; + private int port; + + private static final String hostkey = "src/test/resources/hostkey.pem"; + private static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db"; + + @Before + public void setUp() throws IOException { + ServerSocket s = new ServerSocket(0); + port = s.getLocalPort(); + s.close(); + + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(port); + sshd.setKeyPairProvider(new FileKeyPairProvider(new String[]{hostkey})); + // sshd.setShellFactory(new EchoShellFactory()); + sshd.setPasswordAuthenticator(new BogusPasswordAuthenticator()); + sshd.start(); + + ssh = new SSHClient(); + ssh.addHostKeyVerifier(hostname, port, fingerprint); + } + + @After + public void tearUp() throws IOException, InterruptedException { + ssh.disconnect(); + sshd.stop(); + } + + @Test + public void testAuthenticate() throws IOException { + connect(); + authenticate(); + assertTrue(ssh.isAuthenticated()); + } + + @Test + public void testConnect() throws IOException { + connect(); + assertTrue(ssh.isConnected()); + } + + // @Test + // // TODO -- test I/O + // public void testShell() throws IOException + // { + // connect(); + // authenticate(); + // + // Shell shell = ssh.startSession().startShell(); + // assertTrue(shell.isOpen()); + // + // shell.close(); + // assertFalse(shell.isOpen()); + // } + + private void authenticate() throws UserAuthException, TransportException { + ssh.authPassword("same", "same"); + } + + private void connect() throws IOException { + ssh.connect(hostname, port); + } + +} diff --git a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java new file mode 100644 index 00000000..b53e2839 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package net.schmizz.sshj.keyprovider; + +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; +import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; +import net.schmizz.sshj.userauth.password.PasswordFinder; +import net.schmizz.sshj.userauth.password.PasswordUtils; +import net.schmizz.sshj.userauth.password.Resource; +import net.schmizz.sshj.util.KeyUtil; +import org.apache.sshd.common.util.SecurityUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class OpenSSHKeyFileTest { + + static final String g = "23b0484f5ad9cba2b3dba7129419fbec7f8c014e22d3b19de4ebbca20d0ebd2e9f5225dabdd48de75f87e3193377fb1072c08433f82f6e6e581a319d4fc7d283cdcd2ae2000fe572c0a800fd47b7590d6a6afe3df54aedd57696c6538029daebf11d9e277edc0c7e905e237d3b9e6a6f674d83da5cc0131ac0be2e55ac69730e"; + static final String p = "92b746cf7c0e9ea35fd9b09b0c3dbdfde453468984698ff168fefef3f0457d29bcf81c88830ac1099223d00745423e44cdef66f4cdc3fad1d95ce2868b3e885c1d518c9fcda597d5c373f05f6f323553f60bd992404183dab41d82ab6d3b3ecf2dfc3c136fa67c4312ec0b7bbac77a634e1eb5dd9a62efd0ddab477d0b49c0b9"; + static final String q = "96a05e07b9e52d6f1137d11d5d270b568b94162f"; + static final String x = "8981aebb71c60b5951f0ab3ed1a00b5307742f43"; + static final String y = "7e845aada202d31004c52ab170cbe62ce9a962b9f4acbc67a57f62eb090a67b3faa53d38050f87b2b66ddf1185472f27842c3e3e58d025f9148a28f49ebdfb6efefee8ee10fe84a2d56535dddb301dfee15538108639e8a0ec7aa237ddb999f35b6a5c6b875052998233374163ad031f974d29c2631394436ae186b418348193"; + + boolean readyToProvide; + + final char[] correctPassphrase = "test_passphrase".toCharArray(); + final char[] incorrectPassphrase = new char[]{' '}; + + final PasswordFinder onlyGivesWhenReady = new PasswordFinder() { + public char[] reqPassword(Resource resource) { + if (!readyToProvide) + throw new AssertionError("Password requested too soon"); + + return correctPassphrase; + } + + public boolean shouldRetry(Resource resource) { + return false; + } + }; + + int triesLeft = 3; + + final PasswordFinder givesOn3rdTry = new PasswordFinder() { + public char[] reqPassword(Resource resource) { + if (triesLeft == 0) + return correctPassphrase; + else { + triesLeft--; + return incorrectPassphrase; + } + } + + public boolean shouldRetry(Resource resource) { + return triesLeft >= 0; + } + }; + + @Test + public void blankingOut() throws IOException, GeneralSecurityException { + FileKeyProvider dsa = new OpenSSHKeyFile(); + dsa.init(new File("src/test/resources/id_dsa"), PasswordUtils.createOneOff(correctPassphrase)); + assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate()); + + char[] blank = new char[correctPassphrase.length]; + Arrays.fill(blank, ' '); + assertArrayEquals(blank, correctPassphrase); + } + + @Test + public void getters() throws IOException, GeneralSecurityException { + FileKeyProvider dsa = new OpenSSHKeyFile(); + dsa.init(new File("src/test/resources/id_dsa"), onlyGivesWhenReady); + assertEquals(dsa.getType(), KeyType.DSA); + assertEquals(KeyUtil.newDSAPublicKey(y, p, q, g), dsa.getPublic()); + readyToProvide = true; + assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate()); + } + + @Test + public void retries() throws IOException, GeneralSecurityException { + FileKeyProvider dsa = new OpenSSHKeyFile(); + dsa.init(new File("src/test/resources/id_dsa"), givesOn3rdTry); + assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate()); + } + + @Before + public void setup() throws UnsupportedEncodingException, GeneralSecurityException { + if (!SecurityUtils.isBouncyCastleRegistered()) + throw new AssertionError("bouncy castle needed"); + } + +} \ No newline at end of file diff --git a/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java new file mode 100644 index 00000000..90a8fb46 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/keyprovider/PKCS8KeyFileTest.java @@ -0,0 +1,62 @@ +/* + * 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 net.schmizz.sshj.keyprovider; + +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SecurityUtils; +import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider; +import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile; +import net.schmizz.sshj.util.KeyUtil; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; + +import static org.junit.Assert.assertEquals; + +public class PKCS8KeyFileTest { + + static final FileKeyProvider rsa = new PKCS8KeyFile(); + + static final String modulus = "a19f65e93926d9a2f5b52072db2c38c54e6cf0113d31fa92ff827b0f3bec609c45ea84264c88e64adba11ff093ed48ee0ed297757654b0884ab5a7e28b3c463bc9074b32837a2b69b61d914abf1d74ccd92b20fa44db3b31fb208c0dd44edaeb4ab097118e8ee374b6727b89ad6ce43f1b70c5a437ccebc36d2dad8ae973caad15cd89ae840fdae02cae42d241baef8fda8aa6bbaa54fd507a23338da6f06f61b34fb07d560e63fbce4a39c073e28573c2962cedb292b14b80d1b4e67b0465f2be0e38526232d0a7f88ce91a055fde082038a87ed91f3ef5ff971e30ea6cccf70d38498b186621c08f8fdceb8632992b480bf57fc218e91f2ca5936770fe9469"; + static final String pubExp = "23"; + static final String privExp = "57bcee2e2656eb2c94033d802dd62d726c6705fabad1fd0df86b67600a96431301620d395cbf5871c7af3d3974dfe5c30f5c60d95d7e6e75df69ed6c5a36a9c8aef554b5058b76a719b8478efa08ad1ebf08c8c25fe4b9bc0bfbb9be5d4f60e6213b4ab1c26ad33f5bba7d93e1cd65f65f5a79eb6ebfb32f930a2b0244378b4727acf83b5fb376c38d4abecc5dc3fc399e618e792d4c745d2dbbb9735242e5033129f2985ca3e28fa33cad2afe3e70e1b07ed2b6ec8a3f843fb4bffe3385ad211c6600618488f4ac70397e8eb036b82d811283dc728504cddbe1533c4dd31b1ec99ffa74fd0e3883a9cb3e2315cc1a56f55d38ed40520dd9ec91b4d2dd790d1b"; + + @Before + public void setUp() throws UnsupportedEncodingException, GeneralSecurityException { + if (!SecurityUtils.isBouncyCastleRegistered()) + throw new AssertionError("bouncy castle needed"); + rsa.init(new File("src/test/resources/id_rsa")); + } + + @Test + public void testKeys() throws IOException, GeneralSecurityException { + assertEquals(KeyUtil.newRSAPublicKey(modulus, pubExp), rsa.getPublic()); + assertEquals(KeyUtil.newRSAPrivateKey(modulus, privExp), rsa.getPrivate()); + } + + @Test + public void testType() throws IOException { + assertEquals(rsa.getType(), KeyType.RSA); + } + +} diff --git a/src/test/java/net/schmizz/sshj/transport/verification/KnownHostsTest.java b/src/test/java/net/schmizz/sshj/transport/verification/KnownHostsTest.java new file mode 100644 index 00000000..5e41a7df --- /dev/null +++ b/src/test/java/net/schmizz/sshj/transport/verification/KnownHostsTest.java @@ -0,0 +1,62 @@ +/* + * 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 net.schmizz.sshj.transport.verification; + +import net.schmizz.sshj.util.KeyUtil; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.PublicKey; + +import static org.junit.Assert.assertTrue; + +public class KnownHostsTest { + + // static { + // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); + // } + + private OpenSSHKnownHosts kh; + + @Before + public void setUp() throws IOException, GeneralSecurityException { + kh = new OpenSSHKnownHosts(new File("src/test/resources/known_hosts")); + } + + @Test + public void testLocalhostEntry() throws UnknownHostException, GeneralSecurityException { + + } + + @Test + public void testSchmizzEntry() throws UnknownHostException, GeneralSecurityException { + final PublicKey key = KeyUtil + .newRSAPublicKey( + "e8ff4797075a861db9d2319960a836b2746ada3da514955d2921f2c6a6c9895cbd557f604e43772b6303e3cab2ad82d83b21acdef4edb72524f9c2bef893335115acacfe2989bcbb2e978e4fedc8abc090363e205d975c1fdc35e55ba4daa4b5d5ab7a22c40f547a4a0fd1c683dfff10551c708ff8c34ea4e175cb9bf2313865308fa23601e5a610e2f76838be7ded3b4d3a2c49d2d40fa20db51d1cc8ab20d330bb0dadb88b1a12853f0ecb7c7632947b098dcf435a54566bcf92befd55e03ee2a57d17524cd3d59d6e800c66059067e5eb6edb81946b3286950748240ec9afa4389f9b62bc92f94ec0fba9e64d6dc2f455f816016a4c5f3d507382ed5d3365", + "23"); + + assertTrue(kh.verify("schmizz.net", 22, key)); + assertTrue(kh.verify("69.163.155.180", 22, key)); + } + +} diff --git a/src/test/java/net/schmizz/sshj/util/BogusPasswordAuthenticator.java b/src/test/java/net/schmizz/sshj/util/BogusPasswordAuthenticator.java new file mode 100644 index 00000000..0bdff239 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/util/BogusPasswordAuthenticator.java @@ -0,0 +1,31 @@ +/* + * 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 net.schmizz.sshj.util; + +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; + +/** Successfully authenticates when username == password. */ +public class BogusPasswordAuthenticator implements PasswordAuthenticator { + + public boolean authenticate(String username, String password, ServerSession s) { + return username.equals(password); + } + +} diff --git a/src/test/java/net/schmizz/sshj/util/BufferTest.java b/src/test/java/net/schmizz/sshj/util/BufferTest.java new file mode 100644 index 00000000..5a44f4de --- /dev/null +++ b/src/test/java/net/schmizz/sshj/util/BufferTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package net.schmizz.sshj.util; + +import net.schmizz.sshj.common.Buffer; +import org.junit.Before; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** Tests {@link net.schmizz.sshj.common.SSHPacket} functionality */ +public class BufferTest { + private Buffer.PlainBuffer posBuf; + private Buffer.PlainBuffer handyBuf; + + @Before + public void setUp() throws UnsupportedEncodingException, GeneralSecurityException { + // for position test + byte[] data = "Hello".getBytes("UTF-8"); + posBuf = new Buffer.PlainBuffer(data); + handyBuf = new Buffer.PlainBuffer(); + } + + @Test + public void testDataTypes() { + // bool + assertEquals(handyBuf.putBoolean(true).readBoolean(), true); + + // byte + assertEquals(handyBuf.putByte((byte) 10).readByte(), (byte) 10); + + // byte array + assertArrayEquals(handyBuf.putBytes("some string".getBytes()).readBytes(), "some string".getBytes()); + + // mpint + BigInteger bi = new BigInteger("1111111111111111111111111111111"); + assertEquals(handyBuf.putMPInt(bi).readMPInt(), bi); + + // string + assertEquals(handyBuf.putString("some string").readString(), "some string"); + + // uint32 + assertEquals(handyBuf.putInt(0xffffffffL).readLong(), 0xffffffffL); + } + + @Test + public void testPassword() { + char[] pass = "lolcatz".toCharArray(); + // test if put correctly as a string + assertEquals(new Buffer.PlainBuffer().putPassword(pass).readString(), "lolcatz"); + // test that char[] was blanked out + assertArrayEquals(pass, " ".toCharArray()); + } + + @Test + public void testPosition() throws UnsupportedEncodingException { + assertEquals(5, posBuf.wpos()); + assertEquals(0, posBuf.rpos()); + assertEquals(5, posBuf.available()); + // read some bytes + byte b = posBuf.readByte(); + assertEquals(b, (byte) 'H'); + assertEquals(1, posBuf.rpos()); + assertEquals(4, posBuf.available()); + } + + @Test + public void testPublickey() { + // TODO stub + } + + @Test + public void testSignature() { + // TODO stub + } + + @Test(expected = Buffer.BufferException.class) + public void testUnderflow() { + // exhaust the buffer + for (int i = 0; i < 5; ++i) + posBuf.readByte(); + // underflow + posBuf.readByte(); + } + +} diff --git a/src/test/java/net/schmizz/sshj/util/KeyUtil.java b/src/test/java/net/schmizz/sshj/util/KeyUtil.java new file mode 100644 index 00000000..2c59d248 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/util/KeyUtil.java @@ -0,0 +1,65 @@ +/* + * 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 net.schmizz.sshj.util; + +import net.schmizz.sshj.common.SecurityUtils; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; + +public class KeyUtil { + + /** Creates a DSA private key. */ + public static PrivateKey newDSAPrivateKey(String x, String p, String q, String g) throws GeneralSecurityException { + return SecurityUtils.getKeyFactory("DSA").generatePrivate(new DSAPrivateKeySpec // + (new BigInteger(x, 16), // + new BigInteger(p, 16), // + new BigInteger(q, 16), // + new BigInteger(g, 16))); + } + + /** Creates a DSA public key. */ + public static PublicKey newDSAPublicKey(String y, String p, String q, String g) throws GeneralSecurityException { + return SecurityUtils.getKeyFactory("DSA").generatePublic(new DSAPublicKeySpec // + (new BigInteger(y, 16), // + new BigInteger(p, 16), // + new BigInteger(q, 16), // + new BigInteger(g, 16))); + } + + /** Creates an RSA private key. */ + public static PrivateKey newRSAPrivateKey(String modulus, String exponent) throws GeneralSecurityException { + return SecurityUtils.getKeyFactory("RSA").generatePrivate(new RSAPrivateKeySpec // + (new BigInteger(modulus, 16), // + new BigInteger(exponent, 16))); + } + + /** Creates an RSA public key. */ + public static PublicKey newRSAPublicKey(String modulus, String exponent) throws GeneralSecurityException { + return SecurityUtils.getKeyFactory("RSA").generatePublic(new RSAPublicKeySpec // + (new BigInteger(modulus, 16), new BigInteger(exponent, 16))); + } + +} diff --git a/src/test/resources/hostkey.pem b/src/test/resources/hostkey.pem new file mode 100644 index 00000000..6b6974c7 --- /dev/null +++ b/src/test/resources/hostkey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDdfIWeSV4o68dRrKSzFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDP +jXgCtlTt3FqTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHj +W5q4OOgWhVvj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQIDAQAB +AoGBANG3JDW6NoP8rF/zXoeLgLCj+tfVUPSczhGFVrQkAk4mWfyRkhN0WlwHFOec +K89MpkV1ij/XPVzU4MNbQ2yod1KiDylzvweYv+EaEhASCmYNs6LS03punml42SL9 +97tOmWfVJXxlQoLiY6jHPU97vTc65k8gL+gmmrpchsW0aqmZAkEA/c8zfmKvY37T +cxcLLwzwsqqH7g2KZGTf9aRmx2ebdW+QKviJJhbdluDgl1TNNFj5vCLznFDRHiqJ +wq0wkZ39cwJBAN9l5v3kdXj21UrurNPdlV0n2GZBt2vblooQC37XHF97r2zM7Ou+ +Lg6MyfJClyguhWL9dxnGbf3btQ0l3KDstxMCQCRaiEqjAfIjWVATzeNIXDWLHXso +b1kf5cA+cwY+vdKdTy4IeUR+Y/DXdvPWDqpf0C11aCVMohdLCn5a5ikFUycCQDhV +K/BuAallJNfmY7JxN87r00fF3ojWMJnT/fIYMFFrkQrwifXQWTDWE76BSDibsosJ +u1TGksnm8zrDh2UVC/0CQFrHTiSl/3DHvWAbOJawGKg46cnlDcAhSyV8Frs8/dlP +7YGG3eqkw++lsghqmFO6mRUTKsBmiiB2wgLGhL5pyYY= +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/id_dsa b/src/test/resources/id_dsa new file mode 100644 index 00000000..8e1759d9 --- /dev/null +++ b/src/test/resources/id_dsa @@ -0,0 +1,15 @@ +-----BEGIN DSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,9B6744BB12A8EA8F + +pzZw5s3zDVHYdejZxdTpaRx00Yd1grbJe6mIJGvZRB0Jm0hKXoOX71PUI814mc5+ +a5pzbyO98aciL/Eat5m3P692WQ0yOPMuphRnklsM3s4qrCjp2aRRbWvbyV/QV9bp +Xz2yYvNqU3WJC3UJIaFFMvRo/lC/Wsz9OvHSSl3LnsXXhiOCeaE32etoOYdlk9ro +N9NqDdaw28t9//iiHhuQK4afK6TZkU6DatFljJHILCC416Xh9+DDK9E+CDKzmlcw +jSwtzgFKEhgrT0XKoZR9LJZDolT1YpFy7M3cFRYIuYvJfuLcjxVEldJE900QlaJS +ybb6RxV6SRVwQYXTbIClcXes+oNJMv59DivAfajxECQC5sAynW/FnY1sz0igmz6D +scclJuJIbawqiuV/Lv6bvgzMa/ZXL4b9JeJPuQELa7tCpvj4fpNk1IiftYISlwoT +iG5pL8yLhPL4/fxGnKJzUPCA9mbwiloW2cAZZxTd+Utqmhemcl9gF0JGNR2XeYwS +3obEjbkqMF0WR3AcVZU9B5d9SKUaAzTp4vu5yZtNVEIaiVlnI3hMwWMs2Jgahswo +QF9MCPsRYsxLs7/u4a4qoQ== +-----END DSA PRIVATE KEY----- diff --git a/src/test/resources/id_dsa.pub b/src/test/resources/id_dsa.pub new file mode 100644 index 00000000..b3330dd2 --- /dev/null +++ b/src/test/resources/id_dsa.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAJK3Rs98Dp6jX9mwmww9vf3kU0aJhGmP8Wj+/vPwRX0pvPgciIMKwQmSI9AHRUI+RM3vZvTNw/rR2Vzihos+iFwdUYyfzaWX1cNz8F9vMjVT9gvZkkBBg9q0HYKrbTs+zy38PBNvpnxDEuwLe7rHemNOHrXdmmLv0N2rR30LScC5AAAAFQCWoF4HueUtbxE30R1dJwtWi5QWLwAAAIAjsEhPWtnLorPbpxKUGfvsf4wBTiLTsZ3k67yiDQ69Lp9SJdq91I3nX4fjGTN3+xBywIQz+C9ublgaMZ1Px9KDzc0q4gAP5XLAqAD9R7dZDWpq/j31Su3VdpbGU4Ap2uvxHZ4nftwMfpBeI307nmpvZ02D2lzAExrAvi5VrGlzDgAAAIB+hFqtogLTEATFKrFwy+Ys6aliufSsvGelf2LrCQpns/qlPTgFD4eytm3fEYVHLyeELD4+WNAl+RSKKPSevftu/v7o7hD+hKLVZTXd2zAd/uFVOBCGOeig7HqiN925mfNbalxrh1BSmYIzN0FjrQMfl00pwmMTlENq4Ya0GDSBkw== passphrase=test_passphrase diff --git a/src/test/resources/id_rsa b/src/test/resources/id_rsa new file mode 100644 index 00000000..8f5dbc48 --- /dev/null +++ b/src/test/resources/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoAIBAAKCAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQm +TIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6 +RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmu +hA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPClizt +spKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmL +GGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQIBIwKCAQBXvO4uJlbrLJQDPYAt +1i1ybGcF+rrR/Q34a2dgCpZDEwFiDTlcv1hxx689OXTf5cMPXGDZXX5udd9p7Wxa +NqnIrvVUtQWLdqcZuEeO+gitHr8IyMJf5Lm8C/u5vl1PYOYhO0qxwmrTP1u6fZPh +zWX2X1p5626/sy+TCisCRDeLRyes+Dtfs3bDjUq+zF3D/DmeYY55LUx0XS27uXNS +QuUDMSnymFyj4o+jPK0q/j5w4bB+0rbsij+EP7S//jOFrSEcZgBhhIj0rHA5fo6w +NrgtgRKD3HKFBM3b4VM8TdMbHsmf+nT9DjiDqcs+IxXMGlb1XTjtQFIN2eyRtNLd +eQ0bAoGBAMwgv3rGytRjVbR4TT77eF81svzitOJWRdfXuKB5gqM3jsPR08f1MEtZ +44kaI5ByJ3mBDt/EwNgLRdmBddPrLu3so9VLdRmWKI+KNGxwkcxzJv1xXdicgw+w +S5WgigJryuUbtdylXQTlRArLUKsXULk/MndhGiD+a4fZ3dUtktF9AoGBAMqxh6tr +S0ao0rN4hc9I92gwPubD1+XQr9TJQEEqGv3g5O3BdfDrTvizfaeNArahrIqyO5SK +7iDg0xcHqdxmVmmCJ8CkIWBPXLU6erQ1QNlBJmnzYn5lR0Ksx2h/myjeXztvJKEM +q4xUjAEzWjmwxxU3Y6l3FokvgIU4kOVoE4JdAoGARfyZa+xizHnUPeAae/5yaclE +rnmdGma43En2KGQsyj7vHpEVaSDdW6nKWuRj9wKRMPkMafpQvxnOzjsD06EXZ4RV +bbN4mw7pVcj8B+wUuyAqoAmchMfya8dqXy+6SfkSXS4Sd4knNODkIPVAOqjoegcJ +/QtZamXbuYyGkjuCy3sCgYBLSUEFJ9ohjymwX/cvvAQfYmCBmTLu9b2mzmhSt94j +yI+Lgl8BtnxrANbmdjQ1NLxuB6+61IRVWtIP3kZnzj1aY4sbqq1PqHLkOkrVOFnq +S2YKGJJMND8KIur67ZFm84J1KUgej61uow9uKQRBUEnx8AByJOsdAwPZteys+sVq +7wKBgAc3BL6ttDVYlL8U8fgvTCGuIajItvOQQj1n8RKyRLblYXBKAtY+pbULzmF+ +HscRgJMcwEIosEbTzzBNEVQm6dS6R/Q534C00Fpsw1z/PFTI8AOdUzTROGjuN8Fg +YZoqMQLhk/PB8V4l7yJmPrE971RmJBBDlLDt6hZwOYEI2yF4 +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/id_rsa.pub b/src/test/resources/id_rsa.pub new file mode 100644 index 00000000..6c50ee23 --- /dev/null +++ b/src/test/resources/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQmTIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmuhA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPCliztspKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmLGGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQ== no-passphrase diff --git a/src/test/resources/known_hosts b/src/test/resources/known_hosts new file mode 100644 index 00000000..5846275f --- /dev/null +++ b/src/test/resources/known_hosts @@ -0,0 +1,3 @@ +schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ== +Above we have a plain line, Below we have a hashed line, This is a garbage line. +|1|dy7xSefq6NmJms6AzANG3w45W28=|SSCTlHs4pZbc2uaRoPvjyEAHE1g= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAu64GJcCkdtckPGt8uKTyhG1ShT1Np1kh10eE49imQ4Nh9Y/IrSPzDtYUAazQ88ABc2NffuOKkdn2qtUwZ1ulfcdNfN3oTim3BiVHqa041pKG0L+onQe8Bo+CaG5KBLy/C24eNGM9EcfQvDQOnq1eD3lnR/l8fFckldzjfxZgar0yT9Bb3pwp50oN+1wSEINJEHOgMIW8kZBQmyNr/B+b7yX+Y1s1vuYIP/i4WimCVmkdi9G87Ga8w7GxKalRD2QOG6Xms2YWRQDN6M/MOn4tda3EKolbWkctEWcQf/PcVJffTH4Wv5f0RjVyrQv4ha4FZcNAv6RkRd9WkiCsiTKioQ==