├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── pastdev │ └── jsch │ ├── DefaultSessionFactory.java │ ├── IOUtils.java │ ├── MultiCloseException.java │ ├── SessionFactory.java │ ├── SessionManager.java │ ├── Slf4jBridge.java │ ├── command │ └── CommandRunner.java │ ├── proxy │ └── SshProxy.java │ ├── scp │ ├── CopyMode.java │ ├── DestinationOs.java │ ├── ScpConnection.java │ ├── ScpEntry.java │ ├── ScpFile.java │ ├── ScpFileInputStream.java │ ├── ScpFileOutputStream.java │ ├── ScpInputStream.java │ ├── ScpMode.java │ └── ScpOutputStream.java │ ├── sftp │ └── SftpRunner.java │ └── tunnel │ ├── Tunnel.java │ ├── TunnelConnection.java │ ├── TunnelConnectionManager.java │ └── TunneledDataSourceWrapper.java └── test ├── java └── com │ └── pastdev │ └── jsch │ ├── ConnectionTest.java │ ├── UriTest.java │ ├── command │ └── CommandRunnerTest.java │ ├── proxy │ └── SshProxyTest.java │ ├── scp │ ├── ScpFileTest.java │ ├── ScpStreamTest.java │ └── ScpTestBase.java │ └── tunnel │ ├── TunnelConnectionManagerTest.java │ ├── TunnelConnectionTest.java │ └── TunneledDataSourceWrapperTest.java └── resources ├── .gitignore ├── configuration.properties_SAMPLE └── logback.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | * eol=lf 3 | 4 | *.7z binary 5 | *.flv binary 6 | *.gif binary 7 | *.ico binary 8 | *.jar binary 9 | *.jpg binary 10 | *.jpeg binary 11 | *.m4v binary 12 | *.mpeg binary 13 | *.mp3 binary 14 | *.mp4 binary 15 | *.mov binary 16 | *.ogg binary 17 | *.png binary 18 | *.tiff binary 19 | *.war binary 20 | *.webm binary 21 | *.zip binary 22 | *.zipx binary 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vi 2 | *~ 3 | .*.swp 4 | 5 | # Eclipse 6 | /.project 7 | /.classpath 8 | /.settings 9 | /data 10 | /target 11 | 12 | # Maven release plugin 13 | /pom.xml.releaseBackup 14 | /release.properties 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lucas Theisen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jsch-extension 2 | ============== 3 | jsch-extension as an extension of the [JSch library](http://www.jcraft.com/jsch/) providing: 4 | * A [session factory](#session-factory) for creating multiple sessions from the same configuration 5 | * A [proxy mechanism](#proxy-mechanism) for the ssh connections allowing multi-hop tunneling 6 | * A [simplified command execution](#simplified-command-execution) interface 7 | * A [simplified sftp](#simplified-sftp) interface 8 | * A [simplified scp](#simplified-scp) interface 9 | * [Tunneling](#tunneling) with simplified configuration and management 10 | 11 | ## Session Factory 12 | A session factory is basically a container for configuration paired with a simple factory for creating `com.jcraft.jsch.Session` objects. It is the core abstraction of the jsch-extension library. The `DefaultSessionFactory` class is a default implementation providing useful configuration options. For example: 13 | 14 | ```java 15 | DefaultSessionFactory defaultSessionFactory = new DefaultSessionFactory( 16 | username, hostname, port ); 17 | try { 18 | defaultSessionFactory.setKnownHosts( knownHosts ); 19 | defaultSessionFactory.setIdentityFromPrivateKey( privateKey ); 20 | } 21 | catch ( JSchException e ) { 22 | Assume.assumeNoException( e ); 23 | } 24 | 25 | ... 26 | 27 | // new session using all defaults 28 | Session session = defaultSessionFactory.newSession(); 29 | 30 | // new session with override for username 31 | Session session2 = defaultSessionFactory.newSessionFactoryBuilder() 32 | .setUsername( "otheruser") 33 | .build() 34 | .newSession(); 35 | ``` 36 | 37 | ## Proxy Mechanism 38 | The proxy allows for multi-hop ssh connections. In other words, if you have a [bastion host](https://en.wikipedia.org/wiki/Bastion_host) type setup, you can tunnel thusly: 39 | 40 | 41 | ```java 42 | SessionFactory proxySessionFactory = sessionFactory 43 | .newSessionFactoryBuilder() 44 | .setHostname( "foo" ) 45 | .setPort( SessionFactory.SSH_PORT ) 46 | .build(); 47 | SessionFactory destinationSessionFactory = sessionFactory 48 | .newSessionFactoryBuilder() 49 | .setProxy( new SshProxy( proxySessionFactory ) ) 50 | .build(); 51 | Session session = destinationSessionFactory.newSession(); 52 | ``` 53 | 54 | Which would ensure any connections to any session created by `destinationSessionFactory` would be tunneled through host `foo`. 55 | 56 | ## Simplified Command Execution 57 | The simplified command execution is provided by the `CommandRunner`. It makes execution of commands on remote systems as simple as: 58 | 59 | ```java 60 | CommandRunner commandRunner = new CommandRunner( sessionFactory ); 61 | ExecuteResult result = commandRunner.execute( "ls -al" ); 62 | String filesInCurrentDirectory = result.getStdout(); 63 | ``` 64 | 65 | ## Simplified `sftp` 66 | The simplified sftp is provided by the `SftpRunner`. This allows direct access to `sftp` commands like this: 67 | 68 | ```java 69 | SftpATTRS stat = null; 70 | new SftpRunner( sessionFactory).execute( new Sftp() { 71 | @Override 72 | public void run( ChannelSftp sftp ) throws IOException { 73 | try { 74 | stat = sftp.lstat( path ); 75 | } 76 | catch ( SftpException e ) { 77 | } 78 | } ); 79 | ``` 80 | 81 | ## Simplified `scp` 82 | The simplified `scp` is provided by the `ScpFile` class. It allows you to copy to/from any file using: 83 | 84 | ```java 85 | File toFile = new File( dir, toFilename ); 86 | try { 87 | ScpFile to = new ScpFile( sessionFactory, 88 | "path", "to", "remote", "file" ); 89 | to.copyFrom( new File( "/path/to/local/file" ); 90 | } 91 | catch ( Exception e ) { 92 | } 93 | ``` 94 | 95 | ## Tunneling 96 | Tunneling is provided by the classes in the `com.pastdev.jsch.tunnel` package. There is support for plain tunneling as well as a convenient wrapper for `javax.sql.DataSource` objects. 97 | 98 | ### Plain tunneling 99 | Opening a tunnel (equivalent to ssh port forwarding `-L foo:1234:bar:1234`) is as simple as: 100 | 101 | ```java 102 | TunnelConnection tunnelConnection = new TunnelConnection( 103 | sessionFactory, 104 | new Tunnel( "foo", 1234, "bar", 1234 ) ); 105 | tunnelConnection.open(); 106 | ``` 107 | 108 | Plain tunneling also offers dynamic local port allocation. Just supply `0` as the local port: 109 | 110 | ```java 111 | TunnelConnection tunnelConnection = new TunnelConnection( 112 | sessionFactory, 113 | new Tunnel( 0, "bar", 1234 ) ); 114 | tunnelConnection.open(); 115 | int assignedPort = tunnelConnection.getTunnel( "bar", 1234 ) 116 | .getAssignedPort(); 117 | ``` 118 | 119 | ### Multiple tunnels 120 | It is often necessary to tunnel multiple ports at the same time. Perhaps you have a web server that you need access to both over http and remote desktop: 121 | 122 | ```java 123 | TunnelConnectionManager manager = new TunnelConnectionManager( 124 | sessionFactory, 125 | "127.0.0.2:80:webserver:80", 126 | "127.0.0.2:13389:webserver:13389" ); 127 | manager.open(); 128 | ``` 129 | 130 | ### Multi-hop tunnels 131 | Sometimes it is necessary to go through multiple servers along the way to your destination. This can be accomplished using a simplified _path and spec_ syntax: 132 | 133 | ```java 134 | TunnelConnectionManager manager = new TunnelConnectionManager( 135 | sessionFactory, 136 | "me@bastion.host->webuser@webserver.gateway|127.0.0.2:80:webserver:80", 137 | "me@bastion.host->webadmin@webserver.gateway|127.0.0.2:13389:webserver:13389" ); 138 | manager.open(); 139 | ``` 140 | 141 | This will tunnel through the bastion host as `me` and, tunnel through the webserver gateway as different users depending on what is being tunneled to. The local ports will then be forwarded from the webserver gateway to the webserver as specified. 142 | 143 | ### DataSource wrapper 144 | The datasource wrapper comes in really handy when your database is locked down behind a firewall with no external connections allowed. Instead you can use an ssh connection the the server and tunnel your database connection through it making it appear as if the connection is local: 145 | 146 | ```java 147 | TunneledDataSourceWrapper wrapper = new TunneledDataSourceWrapper( 148 | new TunnelConnectionManager( 149 | sessionFactory, 150 | pathAndSpecList ), 151 | dataSource ); 152 | ``` 153 | 154 | This wrapper is used exactly like any other `DataSource` and it will manage its own ssh tunnel opening and closing as necessary. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.sonatype.oss 6 | oss-parent 7 | 9 8 | 9 | 10 | com.pastdev 11 | jsch-extension 12 | 0.1.12-SNAPSHOT 13 | jar 14 | 15 | jsch-extension 16 | A set of extensions on top of JSch providing a full SCP protocol implementation, tunneling including multi-hop, a Java 7 FileSystem like implementation for Java 6 and remote command execution 17 | https://github.com/lucastheisen/jsch-extension 18 | 19 | 20 | 21 | MIT license (also X11) 22 | http://www.spdx.org/licenses/MIT 23 | 24 | 25 | 26 | 27 | scm:git:git://github.com/lucastheisen/jsch-extension.git 28 | scm:git:git@github.com:lucastheisen/jsch-extension.git 29 | http://github.com/lucastheisen/jsch-extension 30 | HEAD 31 | 32 | 33 | 34 | pastdev.com 35 | http://pastdev.com 36 | 37 | 38 | 39 | 40 | lucastheisen 41 | Lucas Theisen 42 | lucastheisen@pastdev.com 43 | pastdev.com 44 | 45 | 46 | 47 | 48 | UTF-8 49 | 50 | 0.1.54 51 | 0.0.9 52 | 4.11 53 | 1.1.8 54 | 5.1.40 55 | 1.7.22 56 | 7.0.73 57 | 58 | 59 | 60 | 61 | com.jcraft 62 | jsch 63 | ${jsch.version} 64 | 65 | 66 | com.jcraft 67 | jsch.agentproxy.connector-factory 68 | ${jsch-agentproxy.version} 69 | 70 | 71 | com.jcraft 72 | jsch.agentproxy.jsch 73 | ${jsch-agentproxy.version} 74 | 75 | 76 | org.slf4j 77 | slf4j-api 78 | ${slf4j.version} 79 | 80 | 81 | 82 | junit 83 | junit 84 | ${junit.version} 85 | test 86 | 87 | 88 | ch.qos.logback 89 | logback-classic 90 | ${logback.version} 91 | test 92 | 93 | 94 | mysql 95 | mysql-connector-java 96 | ${mysql.version} 97 | jar 98 | test 99 | 100 | 101 | org.apache.tomcat 102 | tomcat-jdbc 103 | ${tomcat-jdbc.version} 104 | jar 105 | test 106 | 107 | 108 | 109 | 110 | ${project.artifactId} 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-compiler-plugin 115 | 3.3 116 | 117 | 1.7 118 | 1.7 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-release-plugin 124 | 2.5 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-site-plugin 129 | 3.4 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-javadoc-plugin 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/DefaultSessionFactory.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import java.io.File; 5 | import java.io.InputStream; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Vector; 12 | 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | 18 | import com.jcraft.jsch.Identity; 19 | import com.jcraft.jsch.IdentityRepository; 20 | import com.jcraft.jsch.JSch; 21 | import com.jcraft.jsch.JSchException; 22 | import com.jcraft.jsch.Proxy; 23 | import com.jcraft.jsch.Session; 24 | import com.jcraft.jsch.UserInfo; 25 | import com.jcraft.jsch.agentproxy.AgentProxyException; 26 | import com.jcraft.jsch.agentproxy.Connector; 27 | import com.jcraft.jsch.agentproxy.ConnectorFactory; 28 | import com.jcraft.jsch.agentproxy.RemoteIdentityRepository; 29 | 30 | 31 | /** 32 | * The default implementation of {@link com.pastdev.jsch.SessionFactory 33 | * SessionFactory}. This class provides sane defaults for all 34 | * conventional configuration including 35 | * 36 | *

37 | * username: System property user.name
38 | * hostname: localhost
39 | * port: 22
40 | * .ssh directory: System property jsch.dotSsh, or system 41 | * property user.home concatenated with "/.ssh"
42 | * known hosts: System property jsch.knownHosts.file or, 43 | * .ssh directory concatenated with "/known_hosts".
44 | * private keys: First checks for an agent proxy using 45 | * {@link ConnectorFactory#createConnector()}, then system property 46 | * jsch.privateKey.files split on ",", otherwise, .ssh 47 | * directory concatenated with all 3 of "/id_rsa", 48 | * "/id_dsa", and "/id_ecdsa" if they exist. 49 | *

50 | */ 51 | public class DefaultSessionFactory implements SessionFactory { 52 | private static Logger logger = LoggerFactory.getLogger( DefaultSessionFactory.class ); 53 | public static final String PROPERTY_JSCH_DOT_SSH = "jsch.dotSsh"; 54 | public static final String PROPERTY_JSCH_KNOWN_HOSTS_FILE = "jsch.knownHosts.file"; 55 | public static final String PROPERTY_JSCH_PRIVATE_KEY_FILES = "jsch.privateKey.files"; 56 | 57 | private Map config; 58 | private File dotSshDir; 59 | private String hostname; 60 | private JSch jsch; 61 | private String password; 62 | private int port = SSH_PORT; 63 | private Proxy proxy; 64 | private UserInfo userInfo; 65 | private String username; 66 | 67 | /** 68 | * Creates a default DefaultSessionFactory. 69 | */ 70 | public DefaultSessionFactory() { 71 | this( null, null, null ); 72 | } 73 | 74 | /** 75 | * Constructs a DefaultSessionFactory with the supplied properties. 76 | * 77 | * @param username 78 | * The username 79 | * @param hostname 80 | * The hostname 81 | * @param port 82 | * The port 83 | */ 84 | public DefaultSessionFactory( String username, String hostname, Integer port ) { 85 | JSch.setLogger( new Slf4jBridge() ); 86 | jsch = new JSch(); 87 | 88 | try { 89 | setDefaultIdentities(); 90 | } 91 | catch ( JSchException e ) { 92 | logger.warn( "Unable to set default identities: ", e ); 93 | } 94 | 95 | try { 96 | setDefaultKnownHosts(); 97 | } 98 | catch ( JSchException e ) { 99 | logger.warn( "Unable to set default known_hosts: ", e ); 100 | } 101 | 102 | if ( username == null ) { 103 | this.username = System.getProperty( "user.name" ).toLowerCase(); 104 | } 105 | else { 106 | this.username = username; 107 | } 108 | 109 | if ( hostname == null ) { 110 | this.hostname = "localhost"; 111 | } 112 | else { 113 | this.hostname = hostname; 114 | } 115 | 116 | if ( port == null ) { 117 | this.port = 22; 118 | } 119 | else { 120 | this.port = port; 121 | } 122 | } 123 | 124 | private DefaultSessionFactory( JSch jsch, String username, String hostname, int port, Proxy proxy ) { 125 | this.jsch = jsch; 126 | this.username = username; 127 | this.hostname = hostname; 128 | this.port = port; 129 | this.proxy = proxy; 130 | } 131 | 132 | private void clearIdentityRepository() throws JSchException { 133 | jsch.setIdentityRepository( null ); // revert to default identity repo 134 | jsch.removeAllIdentity(); 135 | } 136 | 137 | private File dotSshDir() { 138 | if ( dotSshDir == null ) { 139 | String dotSshString = System.getProperty( PROPERTY_JSCH_DOT_SSH ); 140 | if ( dotSshString != null ) { 141 | dotSshDir = new File( dotSshString ); 142 | } 143 | else { 144 | dotSshDir = new File( 145 | new File( System.getProperty( "user.home" ) ), 146 | ".ssh" ); 147 | } 148 | } 149 | return dotSshDir; 150 | } 151 | 152 | @Override 153 | public String getHostname() { 154 | return hostname; 155 | } 156 | 157 | @Override 158 | public int getPort() { 159 | return port; 160 | } 161 | 162 | @Override 163 | public Proxy getProxy() { 164 | return proxy; 165 | } 166 | 167 | @Override 168 | public String getUsername() { 169 | return username; 170 | } 171 | 172 | @Override 173 | public UserInfo getUserInfo() { 174 | return userInfo; 175 | } 176 | 177 | @Override 178 | public Session newSession() throws JSchException { 179 | Session session = jsch.getSession( username, hostname, port ); 180 | if ( config != null ) { 181 | for ( String key : config.keySet() ) { 182 | session.setConfig( key, config.get( key ) ); 183 | } 184 | } 185 | if ( proxy != null ) { 186 | session.setProxy( proxy ); 187 | } 188 | if ( password != null ) { 189 | session.setPassword( password ); 190 | } 191 | if ( userInfo != null ) { 192 | session.setUserInfo( userInfo ); 193 | } 194 | return session; 195 | } 196 | 197 | @Override 198 | public SessionFactoryBuilder newSessionFactoryBuilder() { 199 | return new SessionFactoryBuilder( jsch, username, hostname, port, proxy, config, userInfo ) { 200 | @Override 201 | public SessionFactory build() { 202 | DefaultSessionFactory sessionFactory = new DefaultSessionFactory( jsch, username, hostname, port, proxy ); 203 | sessionFactory.config = config; 204 | sessionFactory.password = password; 205 | sessionFactory.userInfo = userInfo; 206 | return sessionFactory; 207 | } 208 | }; 209 | } 210 | 211 | /** 212 | * Sets the configuration options for the sessions created by this factory. 213 | * This method will replace the current SessionFactory config 214 | * map. If you want to add, rather than replace, see 215 | * {@link #setConfig(String, String)}. All of these options will be added 216 | * one at a time using 217 | * {@link com.jcraft.jsch.Session#setConfig(String, String) 218 | * Session.setConfig(String, String)}. Details on the supported options can 219 | * be found in the source for {@link com.jcraft.jsch.Session#applyConfig()}. 220 | * 221 | * @param config 222 | * The configuration options 223 | * 224 | * @see com.jcraft.jsch.Session#setConfig(java.util.Hashtable) 225 | * @see com.jcraft.jsch.Session#applyConfig() 226 | */ 227 | public void setConfig( Map config ) { 228 | this.config = config; 229 | } 230 | 231 | /** 232 | * Adds a single configuration options for the sessions created by this 233 | * factory. Details on the supported options can be found in the source for 234 | * {@link com.jcraft.jsch.Session#applyConfig()}. 235 | * 236 | * @param key 237 | * The name of the option 238 | * @param value 239 | * The value of the option 240 | * 241 | * @see #setConfig(Map) 242 | * @see com.jcraft.jsch.Session#setConfig(java.util.Hashtable) 243 | * @see com.jcraft.jsch.Session#applyConfig() 244 | */ 245 | public void setConfig( String key, String value ) { 246 | if ( config == null ) { 247 | config = new HashMap(); 248 | } 249 | config.put( key, value ); 250 | } 251 | 252 | private void setDefaultKnownHosts() throws JSchException { 253 | String knownHosts = System.getProperty( PROPERTY_JSCH_KNOWN_HOSTS_FILE ); 254 | if ( knownHosts != null && !knownHosts.isEmpty() ) { 255 | setKnownHosts( knownHosts ); 256 | } 257 | else { 258 | File knownHostsFile = new File( dotSshDir(), "known_hosts" ); 259 | if ( knownHostsFile.exists() ) { 260 | setKnownHosts( knownHostsFile.getAbsolutePath() ); 261 | } 262 | } 263 | } 264 | 265 | private void setDefaultIdentities() throws JSchException { 266 | boolean identitiesSet = false; 267 | try { 268 | Connector connector = ConnectorFactory.getDefault() 269 | .createConnector(); 270 | if ( connector != null ) { 271 | logger.info( "An AgentProxy Connector was found, check for identities" ); 272 | RemoteIdentityRepository repository = new RemoteIdentityRepository( connector ); 273 | Vector identities = repository.getIdentities(); 274 | if ( identities.size() > 0 ) { 275 | logger.info( "Using AgentProxy identities: {}", identities ); 276 | setIdentityRepository( repository ); 277 | identitiesSet = true; 278 | } 279 | } 280 | } 281 | catch ( AgentProxyException e ) { 282 | logger.debug( "Failed to load any keys from AgentProxy:", e ); 283 | } 284 | if ( !identitiesSet ) { 285 | String privateKeyFilesString = System.getProperty( PROPERTY_JSCH_PRIVATE_KEY_FILES ); 286 | if ( privateKeyFilesString != null && !privateKeyFilesString.isEmpty() ) { 287 | logger.info( "Using local identities from {}: {}", 288 | PROPERTY_JSCH_PRIVATE_KEY_FILES, privateKeyFilesString ); 289 | setIdentitiesFromPrivateKeys( Arrays.asList( privateKeyFilesString.split( "," ) ) ); 290 | identitiesSet = true; 291 | } 292 | } 293 | if ( !identitiesSet ) { 294 | List privateKeyFiles = new ArrayList(); 295 | for ( File file : new File[] { 296 | new File( dotSshDir(), "id_rsa" ), 297 | new File( dotSshDir(), "id_dsa" ), 298 | new File( dotSshDir(), "id_ecdsa" ) } ) { 299 | if ( file.exists() ) { 300 | privateKeyFiles.add( file.getAbsolutePath() ); 301 | } 302 | } 303 | logger.info( "Using local identities: {}", privateKeyFiles ); 304 | setIdentitiesFromPrivateKeys( privateKeyFiles ); 305 | } 306 | } 307 | 308 | /** 309 | * Sets the hostname. 310 | * 311 | * @param hostname 312 | * The hostname. 313 | */ 314 | public void setHostname( String hostname ) { 315 | this.hostname = hostname; 316 | } 317 | 318 | /** 319 | * Configures this factory to use a single identity authenticated by the 320 | * supplied private key. The private key should be the path to a private key 321 | * file in OpenSSH format. Clears out the current {@link IdentityRepository} 322 | * before adding this key. 323 | * 324 | * @param privateKey 325 | * Path to a private key file 326 | * @throws JSchException 327 | * If the key is invalid 328 | */ 329 | public void setIdentityFromPrivateKey( String privateKey ) throws JSchException { 330 | clearIdentityRepository(); 331 | jsch.addIdentity( privateKey ); 332 | } 333 | 334 | /** 335 | * Configures this factory to use a single identity authenticated by the 336 | * supplied private key and pass phrase. The private key should be the path 337 | * to a private key file in OpenSSH format. Clears out the current 338 | * {@link IdentityRepository} before adding this key. 339 | * 340 | * @param privateKey 341 | * Path to a private key file 342 | * @param passPhrase 343 | * Pass phrase for private key 344 | * @throws JSchException 345 | * If the key is invalid 346 | */ 347 | public void setIdentityFromPrivateKey( String privateKey, String passPhrase ) throws JSchException { 348 | clearIdentityRepository(); 349 | jsch.addIdentity( privateKey, passPhrase ); 350 | } 351 | 352 | /** 353 | * Configures this factory to use a list of identities authenticated by the 354 | * supplied private keys. The private keys should be the paths to a private 355 | * key files in OpenSSH format. Clears out the current 356 | * {@link IdentityRepository} before adding these keys. 357 | * 358 | * @param privateKeys 359 | * A list of paths to private key files 360 | * @throws JSchException 361 | * If one (or more) of the keys are invalid 362 | */ 363 | public void setIdentitiesFromPrivateKeys( List privateKeys ) throws JSchException { 364 | clearIdentityRepository(); 365 | for ( String privateKey : privateKeys ) { 366 | jsch.addIdentity( privateKey ); 367 | } 368 | } 369 | 370 | /** 371 | * Sets the {@link IdentityRepository} for this factory. This will replace 372 | * any current IdentityRepository, so you should be sure to call this before 373 | * any of the setIdentit(y|ies)Xxx if you plan on using both. 374 | * 375 | * @param identityRepository 376 | * The identity repository 377 | * 378 | * @see JSch#setIdentityRepository(IdentityRepository) 379 | */ 380 | public void setIdentityRepository( IdentityRepository identityRepository ) { 381 | jsch.setIdentityRepository( identityRepository ); 382 | } 383 | 384 | /** 385 | * Sets the known hosts from the stream. Mostly useful if you distribute 386 | * your known_hosts in the jar for your application rather than allowing 387 | * users to manage their own known hosts. 388 | * 389 | * @param knownHosts 390 | * A stream of known hosts 391 | * @throws JSchException 392 | * If an I/O error occurs 393 | * 394 | * @see JSch#setKnownHosts(InputStream) 395 | */ 396 | public void setKnownHosts( InputStream knownHosts ) throws JSchException { 397 | jsch.setKnownHosts( knownHosts ); 398 | } 399 | 400 | /** 401 | * Sets the known hosts from a file at path knownHosts. 402 | * 403 | * @param knownHosts 404 | * The path to a known hosts file 405 | * @throws JSchException 406 | * If an I/O error occurs 407 | * 408 | * @see JSch#setKnownHosts(String) 409 | */ 410 | public void setKnownHosts( String knownHosts ) throws JSchException { 411 | jsch.setKnownHosts( knownHosts ); 412 | } 413 | 414 | /** 415 | * Sets the {@code password} used to authenticate {@code username}. This 416 | * mode of authentication is not recommended as it would keep the password 417 | * in memory and if the application dies and writes a heap dump, it would be 418 | * available. Using {@link Identity} would be better, or even using ssh 419 | * agent support. 420 | * 421 | * @param password 422 | * the password for {@code username} 423 | */ 424 | public void setPassword( String password ) { 425 | this.password = password; 426 | } 427 | 428 | /** 429 | * Sets the port. 430 | * 431 | * @param port 432 | * The port 433 | */ 434 | public void setPort( int port ) { 435 | this.port = port; 436 | } 437 | 438 | /** 439 | * Sets the proxy through which all connections will be piped. 440 | * 441 | * @param proxy 442 | * The proxy 443 | */ 444 | public void setProxy( Proxy proxy ) { 445 | this.proxy = proxy; 446 | } 447 | 448 | /** 449 | * Sets the {@code UserInfo} for use with {@code keyboard-interactive} 450 | * authentication. This may be useful, however, setting the password 451 | * with {@link #setPassword(String)} is likely sufficient. 452 | * 453 | * @param userInfo 454 | * 455 | * @see Keyboard 457 | * Interactive Authentication Example 458 | */ 459 | public void setUserInfo( UserInfo userInfo ) { 460 | this.userInfo = userInfo; 461 | } 462 | 463 | /** 464 | * Sets the username. 465 | * 466 | * @param username 467 | * The username 468 | */ 469 | public void setUsername( String username ) { 470 | this.username = username; 471 | } 472 | 473 | @Override 474 | public String toString() { 475 | return (proxy == null ? "" : proxy.toString() + " ") + 476 | "ssh://" + username + "@" + hostname + ":" + port; 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/IOUtils.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.nio.ByteBuffer; 12 | import java.nio.channels.Channels; 13 | import java.nio.channels.ReadableByteChannel; 14 | import java.nio.channels.WritableByteChannel; 15 | import java.nio.charset.Charset; 16 | 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | 22 | public class IOUtils { 23 | private static Logger logger = LoggerFactory.getLogger( IOUtils.class ); 24 | 25 | public static void closeAndIgnoreException( Closeable closeable ) { 26 | if ( closeable != null ) { 27 | try { 28 | closeable.close(); 29 | } 30 | catch ( IOException e ) { 31 | } 32 | } 33 | } 34 | 35 | public static void closeAndLogException( Closeable closeable ) { 36 | if ( closeable == null ) { 37 | logger.trace( "closeable was null" ); 38 | } 39 | else { 40 | try { 41 | closeable.close(); 42 | } 43 | catch ( IOException e ) { 44 | if ( logger != null ) { 45 | logger.error( "failed to close InputStream: {}", e.getMessage() ); 46 | logger.debug( "failed to close InputStream:", e ); 47 | } 48 | } 49 | } 50 | } 51 | 52 | public static void copy( InputStream from, OutputStream to ) throws IOException { 53 | ReadableByteChannel in = Channels.newChannel( from ); 54 | WritableByteChannel out = Channels.newChannel( to ); 55 | 56 | final ByteBuffer buffer = ByteBuffer.allocateDirect( 16 * 1024 ); 57 | while ( in.read( buffer ) != -1 ) { 58 | buffer.flip(); 59 | out.write( buffer ); 60 | buffer.compact(); 61 | } 62 | buffer.flip(); 63 | while ( buffer.hasRemaining() ) { 64 | out.write( buffer ); 65 | } 66 | } 67 | 68 | public static void copyFromString( String from, OutputStream to ) throws IOException { 69 | copyFromString( from, Charset.defaultCharset(), to ); 70 | } 71 | 72 | public static void copyFromString( String from, Charset fromCharset, OutputStream to ) throws IOException { 73 | to.write( from.getBytes( fromCharset ) ); 74 | } 75 | 76 | public static String copyToString( InputStream from ) throws IOException { 77 | return copyToString( from, Charset.defaultCharset() ); 78 | } 79 | 80 | public static String copyToString( InputStream from, Charset toCharset ) throws IOException { 81 | StringBuilder builder = new StringBuilder(); 82 | byte[] byteBuffer = new byte[1024]; 83 | int bytesRead = 0; 84 | while ( (bytesRead = from.read( byteBuffer, 0, 1024 )) >= 0 ) { 85 | builder.append( new String( byteBuffer, 0, bytesRead, toCharset ) ); 86 | } 87 | return builder.toString(); 88 | } 89 | 90 | public static void deleteFiles( File... files ) { 91 | for ( File file : files ) { 92 | file.delete(); 93 | } 94 | } 95 | 96 | public static String readFile( File file ) throws IOException { 97 | return readFile( file, Charset.defaultCharset() ); 98 | } 99 | 100 | public static String readFile( File file, Charset charset ) throws IOException { 101 | String contents = null; 102 | InputStream from = null; 103 | try { 104 | from = new FileInputStream( file ); 105 | contents = copyToString( from, charset ); 106 | } 107 | finally { 108 | closeAndLogException( from ); 109 | } 110 | return contents; 111 | } 112 | 113 | public static void writeFile( File file, String contents ) throws IOException { 114 | writeFile( file, contents, Charset.defaultCharset() ); 115 | } 116 | 117 | public static void writeFile( File file, String contents, Charset charset ) throws IOException { 118 | OutputStream outputStream = null; 119 | try { 120 | outputStream = new FileOutputStream( file ); 121 | copyFromString( contents, charset, outputStream ); 122 | } 123 | finally { 124 | closeAndLogException( outputStream ); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/MultiCloseException.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | 9 | public class MultiCloseException extends IOException { 10 | private static final long serialVersionUID = -8654074724588491465L; 11 | 12 | private List causes; 13 | 14 | public void add( Exception e ) { 15 | if ( causes == null ) { 16 | causes = new ArrayList(); 17 | } 18 | causes.add( e ); 19 | } 20 | 21 | public List getCauses() { 22 | return causes; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/SessionFactory.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import java.util.Map; 5 | 6 | 7 | import com.jcraft.jsch.JSch; 8 | import com.jcraft.jsch.JSchException; 9 | import com.jcraft.jsch.Proxy; 10 | import com.jcraft.jsch.Session; 11 | import com.jcraft.jsch.UserInfo; 12 | 13 | 14 | /** 15 | * An interface for creating {@link Session} objects from a common 16 | * configuration. Also supports creation of other SessionFactory instances that 17 | * are initialized from the same configuration and can be modified as necessary. 18 | */ 19 | public interface SessionFactory { 20 | public static final int SSH_PORT = 22; 21 | 22 | /** 23 | * Returns the hostname that sessions built by this factory will connect to. 24 | * 25 | * @return The hostname 26 | */ 27 | public String getHostname(); 28 | 29 | /** 30 | * Returns the port that sessions built by this factory will connect to. 31 | * 32 | * @return The port 33 | */ 34 | public int getPort(); 35 | 36 | /** 37 | * Returns the proxy that sessions built by this factory will connect 38 | * through, if any. If none was configured, null will be 39 | * returned. 40 | * 41 | * @return The proxy or null 42 | */ 43 | public Proxy getProxy(); 44 | 45 | /** 46 | * Returns the username that sessions built by this factory will connect 47 | * with. 48 | * 49 | * @return The username 50 | */ 51 | public String getUsername(); 52 | 53 | /** 54 | * Returns the userInfo that sessions built by this factory will connect 55 | * with. 56 | * 57 | * @return The userInfo 58 | */ 59 | public UserInfo getUserInfo(); 60 | 61 | /** 62 | * Returns a new session using the configured properties. 63 | * 64 | * @return A new session 65 | * @throws JSchException 66 | * If username or hostname are invalid 67 | * 68 | * @see com.jcraft.jsch.JSch#getSession(String, String, int) 69 | */ 70 | public Session newSession() throws JSchException; 71 | 72 | /** 73 | * Returns a builder for another session factory pre-initialized with the 74 | * configuration for this session factory. 75 | * 76 | * @return A builder for a session factory 77 | */ 78 | public SessionFactoryBuilder newSessionFactoryBuilder(); 79 | 80 | abstract public class SessionFactoryBuilder { 81 | protected Map config; 82 | protected String hostname; 83 | protected JSch jsch; 84 | protected int port; 85 | protected Proxy proxy; 86 | protected String username; 87 | protected UserInfo userInfo; 88 | 89 | protected SessionFactoryBuilder( JSch jsch, String username, String hostname, int port, Proxy proxy, Map config, UserInfo userInfo ) { 90 | this.jsch = jsch; 91 | this.username = username; 92 | this.hostname = hostname; 93 | this.port = port; 94 | this.proxy = proxy; 95 | this.config = config; 96 | this.userInfo = userInfo; 97 | } 98 | 99 | /** 100 | * Replaces the current config with config 101 | * 102 | * @param config 103 | * The new config 104 | * @return This builder 105 | * 106 | * @see com.pastdev.jsch.DefaultSessionFactory#setConfig(Map) 107 | */ 108 | public SessionFactoryBuilder setConfig( Map config ) { 109 | this.config = config; 110 | return this; 111 | } 112 | 113 | /** 114 | * Replaces the current hostname with hostname 115 | * 116 | * @param hostname 117 | * The new hostname 118 | * @return This builder 119 | */ 120 | public SessionFactoryBuilder setHostname( String hostname ) { 121 | this.hostname = hostname; 122 | return this; 123 | } 124 | 125 | /** 126 | * Replaces the current port with port 127 | * 128 | * @param port 129 | * The new port 130 | * @return This builder 131 | */ 132 | public SessionFactoryBuilder setPort( int port ) { 133 | this.port = port; 134 | return this; 135 | } 136 | 137 | /** 138 | * Replaces the current proxy with proxy 139 | * 140 | * @param proxy 141 | * The new proxy 142 | * @return This builder 143 | * 144 | * @see com.pastdev.jsch.DefaultSessionFactory#setProxy(Proxy) 145 | */ 146 | public SessionFactoryBuilder setProxy( Proxy proxy ) { 147 | this.proxy = proxy; 148 | return this; 149 | } 150 | 151 | /** 152 | * Replaces the current username with username 153 | * 154 | * @param username 155 | * The new username 156 | * @return This builder 157 | */ 158 | public SessionFactoryBuilder setUsername( String username ) { 159 | this.username = username; 160 | return this; 161 | } 162 | 163 | /** 164 | * Replaces the current userInfo with userInfo 165 | * 166 | * @param userInfo 167 | * The new userInfo 168 | * @return This builder 169 | */ 170 | public SessionFactoryBuilder setUserInfo( UserInfo userInfo ) { 171 | this.userInfo = userInfo; 172 | return this; 173 | } 174 | 175 | /** 176 | * Builds and returns a the new SessionFactory instance. 177 | * 178 | * @return The built SessionFactory 179 | */ 180 | abstract public SessionFactory build(); 181 | } 182 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/SessionManager.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import com.jcraft.jsch.JSchException; 13 | import com.jcraft.jsch.Session; 14 | 15 | 16 | /** 17 | * Provides a convenience wrapper to sessions that maintains the session 18 | * connection for you. Every time you obtain your session through a call to 19 | * {@link #getSession()} the current session will have its connection verified, 20 | * and will reconnect if necessary. 21 | */ 22 | public class SessionManager implements Closeable { 23 | private static final Logger logger = LoggerFactory.getLogger( SessionManager.class ); 24 | 25 | private final SessionFactory sessionFactory; 26 | private Session session; 27 | 28 | /** 29 | * Creates a SessionManager for the supplied sessionFactory. 30 | * 31 | * @param sessionFactory 32 | * The session factory 33 | */ 34 | public SessionManager( SessionFactory sessionFactory ) { 35 | this.sessionFactory = sessionFactory; 36 | } 37 | 38 | @Override 39 | public void close() throws IOException { 40 | if ( session != null && session.isConnected() ) { 41 | session.disconnect(); 42 | } 43 | session = null; 44 | } 45 | 46 | /** 47 | * Returns a connected session. 48 | * 49 | * @return A connected session 50 | * 51 | * @throws JSchException 52 | * If unable to connect the session 53 | */ 54 | public Session getSession() throws JSchException { 55 | if ( session == null || !session.isConnected() ) { 56 | logger.debug( "getting new session from factory session" ); 57 | session = sessionFactory.newSession(); 58 | logger.debug( "connecting session" ); 59 | session.connect(); 60 | } 61 | return session; 62 | } 63 | 64 | /** 65 | * Returns the session factory used by this manager. 66 | * 67 | * @return The session factory 68 | */ 69 | public SessionFactory getSessionFactory() { 70 | return sessionFactory; 71 | } 72 | 73 | @Override 74 | public String toString() { 75 | return sessionFactory.toString(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/Slf4jBridge.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | 8 | /** 9 | * Bridges all JSch logging to the SLF4J API. 10 | */ 11 | public class Slf4jBridge implements com.jcraft.jsch.Logger { 12 | private static Logger logger = LoggerFactory.getLogger( Slf4jBridge.class ); 13 | 14 | public boolean isEnabled( int level ) { 15 | switch ( level ) { 16 | case com.jcraft.jsch.Logger.DEBUG: 17 | return logger.isDebugEnabled(); 18 | case com.jcraft.jsch.Logger.INFO: 19 | return logger.isInfoEnabled(); 20 | case com.jcraft.jsch.Logger.WARN: 21 | return logger.isWarnEnabled(); 22 | case com.jcraft.jsch.Logger.ERROR: 23 | return logger.isErrorEnabled(); 24 | case com.jcraft.jsch.Logger.FATAL: 25 | return true; 26 | default: 27 | return logger.isTraceEnabled(); 28 | } 29 | } 30 | 31 | public void log( int level, String message ) { 32 | switch ( level ) { 33 | case com.jcraft.jsch.Logger.DEBUG: 34 | logger.debug( message ); 35 | break; 36 | case com.jcraft.jsch.Logger.INFO: 37 | logger.info( message ); 38 | break; 39 | case com.jcraft.jsch.Logger.WARN: 40 | logger.warn( message ); 41 | break; 42 | case com.jcraft.jsch.Logger.ERROR: 43 | logger.error( message ); 44 | break; 45 | case com.jcraft.jsch.Logger.FATAL: 46 | logger.error( message ); 47 | break; 48 | default: 49 | logger.trace( message ); 50 | break; 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/command/CommandRunner.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.command; 2 | 3 | 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.nio.charset.Charset; 10 | 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | 16 | import com.jcraft.jsch.ChannelExec; 17 | import com.jcraft.jsch.JSchException; 18 | import com.jcraft.jsch.Session; 19 | import com.pastdev.jsch.IOUtils; 20 | import com.pastdev.jsch.SessionFactory; 21 | import com.pastdev.jsch.SessionManager; 22 | 23 | 24 | /** 25 | * Provides a convenience wrapper around an exec channel. This 26 | * implementation offers a simplified interface to executing remote commands and 27 | * retrieving the results of execution. 28 | * 29 | * @see com.jcraft.jsch.ChannelExec 30 | */ 31 | public class CommandRunner implements Closeable { 32 | private static Logger logger = LoggerFactory.getLogger( CommandRunner.class ); 33 | protected static final Charset UTF8 = Charset.forName( "UTF-8" ); 34 | 35 | protected final SessionManager sessionManager; 36 | 37 | /** 38 | * Creates a new CommandRunner that will use a {@link SessionManager} that 39 | * wraps the supplied sessionFactory. 40 | * 41 | * @param sessionFactory The factory used to create a session manager 42 | */ 43 | public CommandRunner( SessionFactory sessionFactory ) { 44 | this.sessionManager = new SessionManager( sessionFactory ); 45 | } 46 | 47 | /** 48 | * Closes the underlying {@link SessionManager}. 49 | * 50 | * @see SessionManager#close() 51 | */ 52 | @Override 53 | public void close() throws IOException { 54 | sessionManager.close(); 55 | } 56 | 57 | /** 58 | * Returns a new CommandRunner with the same SessionFactory, but will 59 | * create a separate session. 60 | * 61 | * @return A duplicate CommandRunner with a different session. 62 | */ 63 | public CommandRunner duplicate() { 64 | return new CommandRunner( sessionManager.getSessionFactory() ); 65 | } 66 | 67 | /** 68 | * Executes command and returns the result. Use this method 69 | * when the command you are executing requires no input, writes only UTF-8 70 | * compatible text to STDOUT and/or STDERR, and you are comfortable with 71 | * buffering up all of that data in memory. Otherwise, use 72 | * {@link #open(String)}, which allows you to work with the underlying 73 | * streams. 74 | * 75 | * @param command 76 | * The command to execute 77 | * @return The resulting data 78 | * 79 | * @throws JSchException 80 | * If ssh execution fails 81 | * @throws IOException 82 | * If unable to read the result data 83 | */ 84 | public ExecuteResult execute( String command ) throws JSchException, IOException { 85 | logger.debug( "executing {} on {}", command, sessionManager ); 86 | Session session = sessionManager.getSession(); 87 | 88 | ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); 89 | ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); 90 | int exitCode; 91 | ChannelExecWrapper channel = null; 92 | try { 93 | channel = new ChannelExecWrapper( session, command, null, stdOut, stdErr ); 94 | } 95 | finally { 96 | exitCode = channel.close(); 97 | } 98 | 99 | return new ExecuteResult( exitCode, 100 | new String( stdOut.toByteArray(), UTF8 ), 101 | new String( stdErr.toByteArray(), UTF8 ) ); 102 | } 103 | 104 | /** 105 | * Executes command and returns an execution wrapper that 106 | * provides safe access to and management of the underlying streams of data. 107 | * 108 | * @param command 109 | * The command to execute 110 | * @return An execution wrapper that allows you to process the streams 111 | * @throws JSchException 112 | * If ssh execution fails 113 | * @throws IOException 114 | * If unable to read the result data 115 | */ 116 | public ChannelExecWrapper open( String command ) throws JSchException, IOException { 117 | logger.debug( "executing {} on {}", command, sessionManager ); 118 | return new ChannelExecWrapper( sessionManager.getSession(), command, null, null, null ); 119 | } 120 | 121 | /** 122 | * A simple container for the results of a command execution. Contains 123 | *
    124 | *
  • The exit code
  • 125 | *
  • The text written to STDOUT
  • 126 | *
  • The text written to STDERR
  • 127 | *
128 | * The text will be UTF-8 decoded byte data written by the command. 129 | */ 130 | public class ExecuteResult { 131 | private int exitCode; 132 | private String stderr; 133 | private String stdout; 134 | 135 | public ExecuteResult( int exitCode, String stdout, String stderr ) { 136 | this.exitCode = exitCode; 137 | this.stderr = stderr; 138 | this.stdout = stdout; 139 | } 140 | 141 | /** 142 | * Returns the exit code of the command execution. 143 | * 144 | * @return The exit code 145 | */ 146 | public int getExitCode() { 147 | return exitCode; 148 | } 149 | 150 | /** 151 | * Returns the text written to STDERR. This will be a UTF-8 decoding of 152 | * the actual bytes written to STDERR. 153 | * 154 | * @return The text written to STDERR 155 | */ 156 | public String getStderr() { 157 | return stderr; 158 | } 159 | 160 | /** 161 | * Returns the text written to STDOUT. This will be a UTF-8 decoding of 162 | * the actual bytes written to STDOUT. 163 | * 164 | * @return The text written to STDOUT 165 | */ 166 | public String getStdout() { 167 | return stdout; 168 | } 169 | } 170 | 171 | /** 172 | * Wraps the execution of a command to handle the opening and closing of all 173 | * the data streams for you. To use this wrapper, you call 174 | * getXxxStream() for the streams you want to work with, which 175 | * will return an opened stream. Use the stream as needed then call 176 | * {@link ChannelExecWrapper#close() close()} on the ChannelExecWrapper 177 | * itself, which will return the the exit code from the execution of the 178 | * command. 179 | */ 180 | public class ChannelExecWrapper { 181 | protected ChannelExec channel; 182 | protected String command; 183 | protected OutputStream passedInStdErr; 184 | protected InputStream passedInStdIn; 185 | protected OutputStream passedInStdOut; 186 | protected InputStream stdErr; 187 | protected OutputStream stdIn; 188 | protected InputStream stdOut; 189 | 190 | protected ChannelExecWrapper() { 191 | } 192 | 193 | public ChannelExecWrapper( Session session, String command, InputStream stdIn, OutputStream stdOut, OutputStream stdErr ) throws JSchException, IOException { 194 | this.command = command; 195 | this.channel = (ChannelExec) session.openChannel( "exec" ); 196 | if ( stdIn != null ) { 197 | this.passedInStdIn = stdIn; 198 | this.channel.setInputStream( stdIn ); 199 | } 200 | if ( stdOut != null ) { 201 | this.passedInStdOut = stdOut; 202 | this.channel.setOutputStream( stdOut ); 203 | } 204 | if ( stdErr != null ) { 205 | this.passedInStdErr = stdErr; 206 | this.channel.setErrStream( stdErr ); 207 | } 208 | this.channel.setCommand( command ); 209 | this.channel.connect(); 210 | } 211 | 212 | /** 213 | * Safely closes all stream, waits for the underlying connection to 214 | * close, then returns the exit code from the command execution. 215 | * 216 | * @return The exit code from the command execution 217 | */ 218 | public int close() { 219 | int exitCode = -2; 220 | if ( channel != null ) { 221 | try { 222 | // In jsch closing the output stream causes an ssh 223 | // message to get sent in another thread. It returns 224 | // before the message was actually sent. So now i 225 | // wait until the exit status is no longer -1 (active). 226 | IOUtils.closeAndLogException( passedInStdIn ); 227 | IOUtils.closeAndLogException( passedInStdOut ); 228 | IOUtils.closeAndLogException( passedInStdErr ); 229 | IOUtils.closeAndLogException( stdIn ); 230 | IOUtils.closeAndLogException( stdOut ); 231 | IOUtils.closeAndLogException( stdErr ); 232 | int i = 0; 233 | while ( !channel.isClosed() ) { 234 | logger.trace( "waiting for exit {}", i++ ); 235 | try { 236 | Thread.sleep( 100 ); 237 | } 238 | catch ( InterruptedException e ) {} 239 | } 240 | exitCode = channel.getExitStatus(); 241 | } 242 | finally { 243 | if ( channel.isConnected() ) { 244 | channel.disconnect(); 245 | } 246 | } 247 | } 248 | logger.trace( "`{}` exit {}", command, exitCode ); 249 | return exitCode; 250 | } 251 | 252 | /** 253 | * Returns the STDERR stream for you to read from. No need to close this 254 | * stream independently, instead, when done with all processing, call 255 | * {@link #close()}; 256 | * 257 | * @return The STDERR stream 258 | * @throws IOException 259 | * If unable to read from the stream 260 | */ 261 | public InputStream getErrStream() throws IOException { 262 | if ( stdErr == null ) { 263 | stdErr = channel.getErrStream(); 264 | } 265 | return stdErr; 266 | } 267 | 268 | /** 269 | * Returns the STDOUT stream for you to read from. No need to close this 270 | * stream independently, instead, when done with all processing, call 271 | * {@link #close()}; 272 | * 273 | * @return The STDOUT stream 274 | * @throws IOException 275 | * If unable to read from the stream 276 | */ 277 | public InputStream getInputStream() throws IOException { 278 | if ( stdOut == null ) { 279 | stdOut = channel.getInputStream(); 280 | } 281 | return stdOut; 282 | } 283 | 284 | /** 285 | * Returns the STDIN stream for you to write to. No need to close this 286 | * stream independently, instead, when done with all processing, call 287 | * {@link #close()}; 288 | * 289 | * @return The STDIN stream 290 | * @throws IOException 291 | * If unable to write to the stream 292 | */ 293 | public OutputStream getOutputStream() throws IOException { 294 | if ( stdIn == null ) { 295 | stdIn = channel.getOutputStream(); 296 | } 297 | return stdIn; 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/proxy/SshProxy.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.proxy; 2 | 3 | 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.net.Socket; 7 | 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | 13 | import com.jcraft.jsch.Channel; 14 | import com.jcraft.jsch.JSchException; 15 | import com.jcraft.jsch.Proxy; 16 | import com.jcraft.jsch.Session; 17 | import com.jcraft.jsch.SocketFactory; 18 | import com.pastdev.jsch.SessionFactory; 19 | 20 | 21 | public class SshProxy implements Proxy { 22 | private static Logger logger = LoggerFactory.getLogger( SshProxy.class ); 23 | 24 | private Channel channel; 25 | private InputStream inputStream; 26 | private OutputStream outputStream; 27 | private SessionFactory sessionFactory; 28 | private Session session; 29 | 30 | public SshProxy( SessionFactory sessionFactory ) throws JSchException { 31 | this.sessionFactory = sessionFactory; 32 | this.session = sessionFactory.newSession(); 33 | } 34 | 35 | public void close() { 36 | if ( session != null && session.isConnected() ) { 37 | session.disconnect(); 38 | } 39 | } 40 | 41 | public void connect( SocketFactory socketFactory, String host, int port, int timeout ) throws Exception { 42 | logger.debug( "connecting session" ); 43 | session.connect(); 44 | 45 | channel = session.getStreamForwarder( host, port ); 46 | inputStream = channel.getInputStream(); 47 | outputStream = channel.getOutputStream(); 48 | 49 | channel.connect( timeout ); 50 | } 51 | 52 | public InputStream getInputStream() { 53 | return inputStream; 54 | } 55 | 56 | public OutputStream getOutputStream() { 57 | return outputStream; 58 | } 59 | 60 | public Socket getSocket() { 61 | return null; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "PROXY(" + sessionFactory.toString() + ")"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/CopyMode.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | public enum CopyMode { 5 | FILE_ONLY, RECURSIVE 6 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/DestinationOs.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | public enum DestinationOs { 4 | UNIX('/'), 5 | WINDOWS('\\'); 6 | 7 | private char separator; 8 | 9 | private DestinationOs( char separator ) { 10 | this.separator = separator; 11 | } 12 | 13 | public String joinPath( String[] parts ) { 14 | return joinPath( parts, 0, parts.length ); 15 | } 16 | 17 | public String joinPath( String[] parts, int start, int count ) { 18 | StringBuilder builder = new StringBuilder(); 19 | for ( int i = start, end = start + count; i < end; i++ ) { 20 | if ( i > start ) { 21 | builder.append( separator ); 22 | } 23 | builder.append( parts[i] ); 24 | } 25 | return builder.toString(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpConnection.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.nio.charset.Charset; 9 | import java.util.Stack; 10 | 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | 16 | import com.jcraft.jsch.Channel; 17 | import com.jcraft.jsch.ChannelExec; 18 | import com.jcraft.jsch.JSchException; 19 | import com.jcraft.jsch.Session; 20 | import com.pastdev.jsch.SessionFactory; 21 | 22 | 23 | /** 24 | * Based on protocol information found here 27 | * 28 | * @author LTHEISEN 29 | * 30 | */ 31 | public class ScpConnection implements Closeable { 32 | private static Logger logger = LoggerFactory.getLogger( ScpConnection.class ); 33 | private static final Charset US_ASCII = Charset.forName( "US-ASCII" ); 34 | 35 | private Channel channel; 36 | private Stack entryStack; 37 | private InputStream inputStream; 38 | private OutputStream outputStream; 39 | private Session session; 40 | 41 | public ScpConnection( SessionFactory sessionFactory, String path, ScpMode scpMode, CopyMode copyMode ) throws JSchException, IOException { 42 | this.session = sessionFactory.newSession(); 43 | 44 | logger.debug( "connecting session" ); 45 | session.connect(); 46 | 47 | String command = getCommand( path, scpMode, copyMode ); 48 | channel = session.openChannel( "exec" ); 49 | logger.debug( "setting exec command to '{}'", command ); 50 | ((ChannelExec) channel).setCommand( command ); 51 | 52 | logger.debug( "connecting channel" ); 53 | channel.connect(); 54 | 55 | outputStream = channel.getOutputStream(); 56 | inputStream = channel.getInputStream(); 57 | 58 | if ( scpMode == ScpMode.FROM ) { 59 | writeAck(); 60 | } 61 | else if ( scpMode == ScpMode.TO ) { 62 | checkAck(); 63 | } 64 | 65 | this.entryStack = new Stack(); 66 | } 67 | 68 | private static String getCommand( String path, ScpMode scpMode, CopyMode copyMode ) { 69 | StringBuilder command = null; 70 | switch ( scpMode ) { 71 | case TO: 72 | command = new StringBuilder( "scp -tq" ); 73 | break; 74 | case FROM: 75 | command = new StringBuilder( "scp -fq" ); 76 | } 77 | 78 | if ( copyMode == CopyMode.RECURSIVE ) { 79 | command.append( "r" ); 80 | } 81 | 82 | return command.append( " " ).append( path ).toString(); 83 | } 84 | 85 | /** 86 | * Throws an JSchIOException if ack was in error. Ack codes are: 87 | * 88 | *
 89 |      *   0 for success,
 90 |      *   1 for error,
 91 |      *   2 for fatal error
 92 |      * 
93 | * 94 | * Also throws, IOException if unable to read from the InputStream. If 95 | * nothing was thrown, ack was a success. 96 | */ 97 | private int checkAck() throws IOException { 98 | logger.trace( "wait for ack" ); 99 | int b = inputStream.read(); 100 | logger.debug( "ack response: '{}'", b ); 101 | 102 | if ( b == 1 || b == 2 ) { 103 | StringBuilder sb = new StringBuilder(); 104 | int c; 105 | while ( (c = inputStream.read()) != '\n' ) { 106 | sb.append( (char) c ); 107 | } 108 | if ( b == 1 || b == 2 ) { 109 | throw new IOException( sb.toString() ); 110 | } 111 | } 112 | 113 | return b; 114 | } 115 | 116 | public void close() throws IOException { 117 | IOException toThrow = null; 118 | try { 119 | while ( !entryStack.isEmpty() ) { 120 | entryStack.pop().complete(); 121 | } 122 | } 123 | catch ( IOException e ) { 124 | toThrow = e; 125 | } 126 | 127 | try { 128 | if ( outputStream != null ) { 129 | outputStream.close(); 130 | } 131 | } 132 | catch ( IOException e ) { 133 | logger.error( "failed to close outputStream: {}", e.getMessage() ); 134 | logger.debug( "failed to close outputStream:", e ); 135 | } 136 | 137 | try { 138 | if ( inputStream != null ) { 139 | inputStream.close(); 140 | } 141 | } 142 | catch ( IOException e ) { 143 | logger.error( "failed to close inputStream: {}", e.getMessage() ); 144 | logger.debug( "failed to close inputStream:", e ); 145 | } 146 | 147 | if ( channel != null && channel.isConnected() ) { 148 | channel.disconnect(); 149 | } 150 | if ( session != null && session.isConnected() ) { 151 | logger.debug( "disconnecting session" ); 152 | session.disconnect(); 153 | } 154 | 155 | if ( toThrow != null ) { 156 | throw toThrow; 157 | } 158 | } 159 | 160 | public void closeEntry() throws IOException { 161 | entryStack.pop().complete(); 162 | } 163 | 164 | public InputStream getCurrentInputStream() { 165 | if ( entryStack.isEmpty() ) { 166 | return null; 167 | } 168 | CurrentEntry currentEntry = entryStack.peek(); 169 | return (currentEntry instanceof InputStream) ? (InputStream) currentEntry : null; 170 | } 171 | 172 | public OutputStream getCurrentOuputStream() { 173 | if ( entryStack.isEmpty() ) { 174 | return null; 175 | } 176 | CurrentEntry currentEntry = entryStack.peek(); 177 | return (currentEntry instanceof OutputStream) ? (OutputStream) currentEntry : null; 178 | } 179 | 180 | public ScpEntry getNextEntry() throws IOException { 181 | if ( !entryStack.isEmpty() && !entryStack.peek().isDirectoryEntry() ) { 182 | closeEntry(); 183 | } 184 | 185 | ScpEntry entry = parseMessage(); 186 | if ( entry == null ) return null; 187 | if ( entry.isEndOfDirectory() ) { 188 | while ( !entryStack.isEmpty() ) { 189 | boolean isDirectory = entryStack.peek().isDirectoryEntry(); 190 | closeEntry(); 191 | if ( isDirectory ) { 192 | break; 193 | } 194 | } 195 | } 196 | else if ( entry.isDirectory() ) { 197 | entryStack.push( new InputDirectoryEntry( entry ) ); 198 | } 199 | else { 200 | entryStack.push( new EntryInputStream( entry ) ); 201 | } 202 | return entry; 203 | } 204 | 205 | /** 206 | * Parses SCP protocol messages, for example: 207 | * 208 | *
209 |      *     File:          C0640 13 test.txt 
210 |      *     Directory:     D0750 0 testdir 
211 |      *     End Directory: E
212 |      * 
213 | * 214 | * @return An ScpEntry for a file (C), directory (D), end of directory (E), 215 | * or null when no more messages are available. 216 | * @throws IOException 217 | */ 218 | private ScpEntry parseMessage() throws IOException { 219 | int ack = checkAck(); 220 | if ( ack == -1 ) return null; // end of stream 221 | 222 | char type = (char) ack; 223 | 224 | ScpEntry scpEntry = null; 225 | if ( type == 'E' ) { 226 | scpEntry = ScpEntry.newEndOfDirectory(); 227 | readMessageSegment(); // read and discard the \n 228 | } 229 | else if ( type == 'C' || type == 'D' ) { 230 | String mode = readMessageSegment(); 231 | String sizeString = readMessageSegment(); 232 | if ( sizeString == null ) return null; 233 | long size = Long.parseLong( sizeString ); 234 | String name = readMessageSegment(); 235 | if ( name == null ) return null; 236 | 237 | scpEntry = type == 'C' 238 | ? ScpEntry.newFile( name, size, mode ) 239 | : ScpEntry.newDirectory( name, mode ); 240 | } 241 | else { 242 | throw new UnsupportedOperationException( "unknown protocol message type " + type ); 243 | } 244 | 245 | logger.debug( "read '{}'", scpEntry ); 246 | return scpEntry; 247 | } 248 | 249 | public void putNextEntry( String name ) throws IOException { 250 | putNextEntry( ScpEntry.newDirectory( name ) ); 251 | } 252 | 253 | public void putNextEntry( String name, long size ) throws IOException { 254 | putNextEntry( ScpEntry.newFile( name, size ) ); 255 | } 256 | 257 | public void putNextEntry( ScpEntry entry ) throws IOException { 258 | if ( entry.isEndOfDirectory() ) { 259 | while ( !entryStack.isEmpty() ) { 260 | boolean isDirectory = entryStack.peek().isDirectoryEntry(); 261 | closeEntry(); 262 | if ( isDirectory ) { 263 | break; 264 | } 265 | } 266 | return; 267 | } 268 | else if ( !entryStack.isEmpty() ) { 269 | CurrentEntry currentEntry = entryStack.peek(); 270 | if ( !currentEntry.isDirectoryEntry() ) { 271 | // auto close previous file entry 272 | closeEntry(); 273 | } 274 | } 275 | 276 | if ( entry.isDirectory() ) { 277 | entryStack.push( new OutputDirectoryEntry( entry ) ); 278 | } 279 | else { 280 | entryStack.push( new EntryOutputStream( entry ) ); 281 | } 282 | } 283 | 284 | private String readMessageSegment() throws IOException { 285 | byte[] buffer = new byte[1024]; 286 | int bytesRead = 0; 287 | for ( ;; bytesRead++ ) { 288 | byte b = (byte) inputStream.read(); 289 | if ( b == -1 ) return null; // end of stream 290 | if ( b == ' ' || b == '\n' ) break; 291 | buffer[bytesRead] = b; 292 | } 293 | return new String( buffer, 0, bytesRead, US_ASCII ); 294 | } 295 | 296 | private void writeAck() throws IOException { 297 | logger.debug( "writing ack" ); 298 | outputStream.write( (byte) 0 ); 299 | outputStream.flush(); 300 | } 301 | 302 | private void writeMessage( String message ) throws IOException { 303 | writeMessage( message.getBytes( US_ASCII ) ); 304 | } 305 | 306 | private void writeMessage( byte... message ) throws IOException { 307 | if ( logger.isDebugEnabled() ) { 308 | logger.debug( "writing message: '{}'", new String( message, US_ASCII ) ); 309 | } 310 | outputStream.write( message ); 311 | outputStream.flush(); 312 | checkAck(); 313 | } 314 | 315 | private interface CurrentEntry { 316 | public void complete() throws IOException; 317 | 318 | public boolean isDirectoryEntry(); 319 | } 320 | 321 | private class InputDirectoryEntry implements CurrentEntry { 322 | private InputDirectoryEntry( ScpEntry entry ) throws IOException { 323 | writeAck(); 324 | } 325 | 326 | public void complete() throws IOException { 327 | writeAck(); 328 | } 329 | 330 | public boolean isDirectoryEntry() { 331 | return true; 332 | } 333 | } 334 | 335 | private class OutputDirectoryEntry implements CurrentEntry { 336 | private OutputDirectoryEntry( ScpEntry entry ) throws IOException { 337 | writeMessage( "D" + entry.getMode() + " 0 " + entry.getName() + "\n" ); 338 | } 339 | 340 | public void complete() throws IOException { 341 | writeMessage( "E\n" ); 342 | } 343 | 344 | public boolean isDirectoryEntry() { 345 | return true; 346 | } 347 | } 348 | 349 | private class EntryInputStream extends InputStream implements CurrentEntry { 350 | private ScpEntry entry; 351 | private long ioCount; 352 | private boolean closed; 353 | 354 | public EntryInputStream( ScpEntry entry ) throws IOException { 355 | this.entry = entry; 356 | this.ioCount = 0L; 357 | 358 | writeAck(); 359 | this.closed = false; 360 | } 361 | 362 | @Override 363 | public void close() throws IOException { 364 | if ( !closed ) { 365 | if ( !isComplete() ) { 366 | throw new IOException( "stream not finished (" 367 | + ioCount + "!=" + entry.getSize() + ")" ); 368 | } 369 | writeAck(); 370 | checkAck(); 371 | this.closed = true; 372 | } 373 | } 374 | 375 | public void complete() throws IOException { 376 | close(); 377 | } 378 | 379 | private void increment() throws IOException { 380 | ioCount++; 381 | } 382 | 383 | private boolean isComplete() { 384 | return ioCount == entry.getSize(); 385 | } 386 | 387 | public boolean isDirectoryEntry() { 388 | return false; 389 | } 390 | 391 | @Override 392 | public int read() throws IOException { 393 | if ( isComplete() ) { 394 | return -1; 395 | } 396 | increment(); 397 | return inputStream.read(); 398 | } 399 | } 400 | 401 | private class EntryOutputStream extends OutputStream implements CurrentEntry { 402 | private ScpEntry entry; 403 | private long ioCount; 404 | private boolean closed; 405 | 406 | public EntryOutputStream( ScpEntry entry ) throws IOException { 407 | this.entry = entry; 408 | this.ioCount = 0L; 409 | 410 | writeMessage( "C" + entry.getMode() + " " + entry.getSize() + " " + entry.getName() + "\n" ); 411 | this.closed = false; 412 | } 413 | 414 | @Override 415 | public void close() throws IOException { 416 | if ( !closed ) { 417 | if ( !isComplete() ) { 418 | throw new IOException( "stream not finished (" 419 | + ioCount + "!=" + entry.getSize() + ")" ); 420 | } 421 | writeMessage( (byte) 0 ); 422 | this.closed = true; 423 | } 424 | } 425 | 426 | public void complete() throws IOException { 427 | close(); 428 | } 429 | 430 | private void increment() throws IOException { 431 | if ( isComplete() ) { 432 | throw new IOException( "too many bytes written for file " + entry.getName() ); 433 | } 434 | ioCount++; 435 | } 436 | 437 | private boolean isComplete() { 438 | return ioCount == entry.getSize(); 439 | } 440 | 441 | public boolean isDirectoryEntry() { 442 | return false; 443 | } 444 | 445 | @Override 446 | public void write( int b ) throws IOException { 447 | increment(); 448 | outputStream.write( b ); 449 | } 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpEntry.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | import java.io.IOException; 4 | import java.util.regex.Pattern; 5 | 6 | 7 | public class ScpEntry { 8 | private static final String DEFAULT_DIRECTORY_MODE = "0750"; 9 | private static final String DEFAULT_FILE_MODE = "0640"; 10 | private static final Pattern MODE_PATTERN = Pattern.compile( "[0-2]?[0-7]{3}" ); 11 | 12 | private String mode; 13 | private String name; 14 | private long size; 15 | private Type type; 16 | 17 | private ScpEntry( String name, long size, String mode, Type type ) throws IOException { 18 | this.name = name; 19 | this.size = size; 20 | this.mode = type == Type.END_OF_DIRECTORY ? null : standardizeMode( mode ); 21 | this.type = type; 22 | } 23 | 24 | public String getMode() { 25 | return mode; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public long getSize() { 33 | return size; 34 | } 35 | 36 | public boolean isDirectory() { 37 | return type == Type.DIRECTORY; 38 | } 39 | 40 | public boolean isEndOfDirectory() { 41 | return type == Type.END_OF_DIRECTORY; 42 | } 43 | 44 | public boolean isFile() { 45 | return type == Type.FILE; 46 | } 47 | 48 | public static ScpEntry newDirectory( String name ) throws IOException { 49 | return newDirectory( name, DEFAULT_DIRECTORY_MODE ); 50 | } 51 | 52 | public static ScpEntry newDirectory( String name, String mode ) throws IOException { 53 | return new ScpEntry( name, 0L, mode, Type.DIRECTORY ); 54 | } 55 | 56 | public static ScpEntry newEndOfDirectory() throws IOException { 57 | return new ScpEntry( null, 0L, null, Type.END_OF_DIRECTORY ); 58 | } 59 | 60 | public static ScpEntry newFile( String name, long size ) throws IOException { 61 | return newFile( name, size, DEFAULT_FILE_MODE ); 62 | } 63 | 64 | public static ScpEntry newFile( String name, long size, String mode ) throws IOException { 65 | return new ScpEntry( name, size, mode, Type.FILE ); 66 | } 67 | 68 | private static String standardizeMode( String mode ) throws IOException { 69 | if ( !MODE_PATTERN.matcher( mode ).matches() ) { 70 | throw new IOException( "invalid file mode " + mode ); 71 | } 72 | if ( mode.length() == 3 ) { 73 | mode = "0" + mode; 74 | } 75 | return mode; 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | switch ( type ) { 81 | case FILE: return "C" + mode + " " + size + " " + name; 82 | case DIRECTORY: return "D" + mode + " " + size + " " + name; 83 | case END_OF_DIRECTORY: return "E"; 84 | default: return "Weird, I have no idea how this happened..."; 85 | } 86 | } 87 | 88 | public enum Type { 89 | FILE, DIRECTORY, END_OF_DIRECTORY 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpFile.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | 9 | 10 | import com.jcraft.jsch.JSchException; 11 | import com.pastdev.jsch.IOUtils; 12 | import com.pastdev.jsch.SessionFactory; 13 | 14 | 15 | public class ScpFile { 16 | private DestinationOs os; 17 | private String[] path; 18 | private SessionFactory sessionFactory; 19 | 20 | public ScpFile( SessionFactory sessionFactory, String... path ) { 21 | this( sessionFactory, DestinationOs.UNIX, path ); 22 | } 23 | 24 | public ScpFile( SessionFactory sessionFactory, DestinationOs os, String... path ) { 25 | this.sessionFactory = sessionFactory; 26 | this.os = os; 27 | this.path = path; 28 | } 29 | 30 | public void copyFrom( File file ) throws IOException, JSchException { 31 | copyFrom( file, null ); 32 | } 33 | 34 | public void copyFrom( File file, String mode ) throws IOException, JSchException { 35 | FileInputStream from = null; 36 | ScpFileOutputStream to = null; 37 | try { 38 | from = new FileInputStream( file ); 39 | to = mode == null 40 | ? getOutputStream( file.length() ) 41 | : getOutputStream( file.length(), mode ); 42 | IOUtils.copy( from, to ); 43 | } 44 | finally { 45 | if ( from != null ) { 46 | IOUtils.closeAndLogException( from ); 47 | } 48 | if ( to != null ) { 49 | IOUtils.closeAndLogException( to ); 50 | } 51 | } 52 | 53 | } 54 | 55 | public void copyTo( File file ) throws JSchException, IOException { 56 | ScpFileInputStream from = null; 57 | FileOutputStream to = null; 58 | try { 59 | from = getInputStream(); 60 | String name = from.getName(); 61 | String mode = from.getMode(); 62 | if ( file.isDirectory() ) { 63 | file = new File( file, name ); 64 | } 65 | to = new FileOutputStream( file ); 66 | 67 | // attempt to set file mode... flakey in java 6 and below 68 | int userPerm = Character.getNumericValue( mode.charAt( 1 ) ); 69 | int otherPerm = Character.getNumericValue( mode.charAt( 3 ) ); 70 | if ( (userPerm & 1) == 1 ) { 71 | if ( (otherPerm & 1) == 1 ) { 72 | file.setExecutable( true, false ); 73 | } 74 | else { 75 | file.setExecutable( true, true ); 76 | } 77 | } 78 | if ( (userPerm & 2) == 2 ) { 79 | if ( (otherPerm & 2) == 2 ) { 80 | file.setWritable( true, false ); 81 | } 82 | else { 83 | file.setWritable( true, true ); 84 | } 85 | } 86 | if ( (userPerm & 4) == 4 ) { 87 | if ( (otherPerm & 4) == 4 ) { 88 | file.setReadable( true, false ); 89 | } 90 | else { 91 | file.setReadable( true, true ); 92 | } 93 | } 94 | 95 | IOUtils.copy( from, to ); 96 | } 97 | finally { 98 | if ( from != null ) { 99 | IOUtils.closeAndLogException( from ); 100 | } 101 | if ( to != null ) { 102 | IOUtils.closeAndLogException( to ); 103 | } 104 | } 105 | } 106 | 107 | public void copyTo( ScpFile file ) throws JSchException, IOException { 108 | ScpFileInputStream from = null; 109 | ScpFileOutputStream to = null; 110 | try { 111 | from = getInputStream(); 112 | String mode = from.getMode(); 113 | long size = from.getSize(); 114 | to = file.getOutputStream( size, mode ); 115 | 116 | IOUtils.copy( from, to ); 117 | } 118 | finally { 119 | if ( from != null ) { 120 | IOUtils.closeAndLogException( from ); 121 | } 122 | if ( to != null ) { 123 | IOUtils.closeAndLogException( to ); 124 | } 125 | } 126 | } 127 | 128 | public ScpFileInputStream getInputStream() throws JSchException, IOException { 129 | return new ScpFileInputStream( sessionFactory, getPath() ); 130 | } 131 | 132 | public ScpFileOutputStream getOutputStream( long size ) throws JSchException, IOException { 133 | return getOutputStream( ScpEntry.newFile( getFilename(), size ) ); 134 | } 135 | 136 | public ScpFileOutputStream getOutputStream( long size, String mode ) throws JSchException, IOException { 137 | return getOutputStream( ScpEntry.newFile( getFilename(), size, mode ) ); 138 | } 139 | 140 | private ScpFileOutputStream getOutputStream( ScpEntry scpEntry ) throws JSchException, IOException { 141 | return new ScpFileOutputStream( sessionFactory, getDirectory(), scpEntry ); 142 | } 143 | 144 | String getDirectory() { 145 | return os.joinPath( path, 0, path.length - 1 ); 146 | } 147 | 148 | String getFilename() { 149 | return path[path.length - 1]; 150 | } 151 | 152 | String getPath() { 153 | return os.joinPath( path, 0, path.length ); 154 | } 155 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpFileInputStream.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import com.jcraft.jsch.JSchException; 13 | import com.pastdev.jsch.SessionFactory; 14 | 15 | 16 | public class ScpFileInputStream extends InputStream { 17 | private static Logger logger = LoggerFactory.getLogger( ScpFileInputStream.class ); 18 | 19 | private ScpInputStream inputStream; 20 | private ScpEntry scpEntry; 21 | 22 | ScpFileInputStream( SessionFactory sessionFactory, String path ) throws JSchException, IOException { 23 | logger.debug( "Opening ScpInputStream to {} {}", sessionFactory, path ); 24 | this.inputStream = new ScpInputStream( sessionFactory, path, CopyMode.FILE_ONLY ); 25 | this.scpEntry = this.inputStream.getNextEntry(); 26 | } 27 | 28 | public String getMode() { 29 | return scpEntry.getMode(); 30 | } 31 | 32 | public String getName() { 33 | return scpEntry.getName(); 34 | } 35 | 36 | public long getSize() { 37 | return scpEntry.getSize(); 38 | } 39 | 40 | @Override 41 | public void close() throws IOException { 42 | logger.debug( "Closing ScpInputStream" ); 43 | inputStream.closeEntry(); 44 | inputStream.close(); 45 | } 46 | 47 | @Override 48 | public int read() throws IOException { 49 | return inputStream.read(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpFileOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | 8 | import com.jcraft.jsch.JSchException; 9 | import com.pastdev.jsch.SessionFactory; 10 | 11 | 12 | public class ScpFileOutputStream extends OutputStream { 13 | private ScpOutputStream outputStream; 14 | 15 | ScpFileOutputStream( SessionFactory sessionFactory, String directory, ScpEntry scpEntry ) throws JSchException, IOException { 16 | this.outputStream = new ScpOutputStream( sessionFactory, directory, CopyMode.FILE_ONLY ); 17 | this.outputStream.putNextEntry( scpEntry ); 18 | } 19 | 20 | @Override 21 | public void close() throws IOException { 22 | outputStream.closeEntry(); 23 | outputStream.close(); 24 | } 25 | 26 | @Override 27 | public void write( int b ) throws IOException { 28 | outputStream.write( b ); 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpInputStream.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import com.jcraft.jsch.JSchException; 13 | import com.pastdev.jsch.SessionFactory; 14 | 15 | 16 | public class ScpInputStream extends InputStream { 17 | private static Logger logger = LoggerFactory.getLogger( ScpInputStream.class ); 18 | 19 | private ScpConnection connection; 20 | private InputStream inputStream; 21 | 22 | public ScpInputStream( SessionFactory sessionFactory, String path, CopyMode copyMode ) throws JSchException, IOException { 23 | logger.debug( "Opening ScpInputStream" ); 24 | this.connection = new ScpConnection( sessionFactory, path, ScpMode.FROM, copyMode ); 25 | } 26 | 27 | @Override 28 | public void close() throws IOException { 29 | logger.debug( "Closing ScpInputStream" ); 30 | connection.close(); 31 | inputStream = null; 32 | } 33 | 34 | public void closeEntry() throws IOException { 35 | connection.closeEntry(); 36 | inputStream = null; 37 | } 38 | 39 | public ScpEntry getNextEntry() throws IOException { 40 | ScpEntry entry = connection.getNextEntry(); 41 | inputStream = connection.getCurrentInputStream(); 42 | return entry; 43 | } 44 | 45 | @Override 46 | public int read() throws IOException { 47 | if ( inputStream == null ) { 48 | throw new IllegalStateException( "no current entry, cannot read" ); 49 | } 50 | return inputStream.read(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpMode.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | public enum ScpMode { 4 | TO, FROM 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/scp/ScpOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import com.jcraft.jsch.JSchException; 13 | import com.pastdev.jsch.SessionFactory; 14 | 15 | 16 | /** 17 | * Based upon information found here. 20 | * 21 | * @author ltheisen 22 | * 23 | */ 24 | public class ScpOutputStream extends OutputStream { 25 | private static Logger logger = LoggerFactory.getLogger( ScpOutputStream.class ); 26 | 27 | private ScpConnection connection; 28 | private OutputStream outputStream; 29 | 30 | public ScpOutputStream( SessionFactory sessionFactory, String path, CopyMode copyMode ) throws JSchException, IOException { 31 | logger.debug( "Opening ScpOutputStream to {} {}", sessionFactory, path ); 32 | this.connection = new ScpConnection( sessionFactory, path, ScpMode.TO, copyMode ); 33 | } 34 | 35 | @Override 36 | public void close() throws IOException { 37 | logger.debug( "Closing ScpOutputStream" ); 38 | connection.close(); 39 | outputStream = null; 40 | } 41 | 42 | public void closeEntry() throws IOException { 43 | connection.closeEntry(); 44 | outputStream = null; 45 | } 46 | 47 | public void putNextEntry( String name ) throws IOException { 48 | connection.putNextEntry( ScpEntry.newDirectory( name ) ); 49 | outputStream = connection.getCurrentOuputStream(); 50 | } 51 | 52 | public void putNextEntry( String name, long size ) throws IOException { 53 | connection.putNextEntry( ScpEntry.newFile( name, size ) ); 54 | outputStream = connection.getCurrentOuputStream(); 55 | } 56 | 57 | public void putNextEntry( ScpEntry entry ) throws IOException { 58 | connection.putNextEntry( entry ); 59 | outputStream = connection.getCurrentOuputStream(); 60 | } 61 | 62 | @Override 63 | public void write( int b ) throws IOException { 64 | if ( outputStream == null ) { 65 | throw new IllegalStateException( "no current entry, cannot write" ); 66 | } 67 | outputStream.write( b ); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/sftp/SftpRunner.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.sftp; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | 12 | import com.jcraft.jsch.ChannelSftp; 13 | import com.jcraft.jsch.JSchException; 14 | import com.pastdev.jsch.SessionFactory; 15 | import com.pastdev.jsch.SessionManager; 16 | 17 | 18 | /** 19 | * Provides a convenience wrapper around an sftp channel. This 20 | * implementation offers a simplified interface that manages the resources 21 | * needed to issue sftp commands. 22 | * 23 | * @see com.jcraft.jsch.ChannelSftp 24 | */ 25 | public class SftpRunner implements Closeable { 26 | private static final Logger logger = LoggerFactory.getLogger( SftpRunner.class ); 27 | private static final String CHANNEL_SFTP = "sftp"; 28 | 29 | private final SessionManager sessionManager; 30 | 31 | /** 32 | * Creates a new SftpRunner that will use a {@link SessionManager} that 33 | * wraps the supplied sessionFactory. 34 | * 35 | * @param sessionFactory 36 | * The factory used to create a session manager 37 | */ 38 | public SftpRunner( SessionFactory sessionFactory ) { 39 | this.sessionManager = new SessionManager( sessionFactory ); 40 | } 41 | 42 | /** 43 | * Executes the sftp callback providing it an open 44 | * {@link ChannelSftp}. Sftp callback implementations should NOT 45 | * close the channel. 46 | * 47 | * @param sftp A callback 48 | * @throws JSchException 49 | * If ssh execution fails 50 | * @throws IOException 51 | * If unable to read the result data 52 | */ 53 | public void execute( Sftp sftp ) throws JSchException, IOException { 54 | logger.debug( "executing sftp command on {}", sessionManager ); 55 | ChannelSftp channelSftp = null; 56 | try { 57 | channelSftp = (ChannelSftp) sessionManager.getSession() 58 | .openChannel( CHANNEL_SFTP ); 59 | channelSftp.connect(); 60 | sftp.run( channelSftp ); 61 | } 62 | finally { 63 | if ( channelSftp != null ) { 64 | channelSftp.disconnect(); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Closes the underlying {@link SessionManager}. 71 | * 72 | * @see SessionManager#close() 73 | */ 74 | @Override 75 | public void close() throws IOException { 76 | sessionManager.close(); 77 | } 78 | 79 | /** 80 | * A simple callback interface for working with managed sftp channels. 81 | */ 82 | public static interface Sftp { 83 | public void run( ChannelSftp sftp ) throws JSchException, IOException; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/tunnel/Tunnel.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | /** 5 | * Tunnel stores all the information needed to define an ssh port-forwarding 6 | * tunnel. 7 | * 8 | * @see rfc4254 9 | */ 10 | public class Tunnel { 11 | private String spec; 12 | private String destinationHostname; 13 | private int destinationPort; 14 | private String localAlias; 15 | private int localPort; 16 | private int assignedLocalPort; 17 | 18 | /** 19 | * Creates a Tunnel from a spec string. For details on this 20 | * string, see {@link #getSpec()}. 21 | *

22 | * Both localAlias and localPort are optional, in 23 | * which case they default to localhost and 0 24 | * respectively. 25 | *

26 | *

27 | * Examples: 28 | * 29 | *

 30 |      * // Equivalaent to new Tunnel("localhost", 0, "foobar", 1234);
 31 |      * new Tunnel( "foobar:1234" );
 32 |      * // Equivalaent to new Tunnel("localhost", 1234, "foobar", 1234);
 33 |      * new Tunnel( "1234:foobar:1234" );
 34 |      * // Equivalaent to new Tunnel("local_foobar", 1234, "foobar", 1234);
 35 |      * new Tunnel( "local_foobar:1234:foobar:1234" );
 36 |      * 
37 | * 38 | * @param spec A tunnel spec string 39 | * 40 | * @see #Tunnel(String, int, String, int) 41 | * @see rfc4254 42 | */ 43 | public Tunnel( String spec ) { 44 | String[] parts = spec.split( ":" ); 45 | if ( parts.length == 4 ) { 46 | this.localAlias = parts[0]; 47 | this.localPort = Integer.parseInt( parts[1] ); 48 | this.destinationHostname = parts[2]; 49 | this.destinationPort = Integer.parseInt( parts[3] ); 50 | } 51 | else if ( parts.length == 3 ) { 52 | this.localPort = Integer.parseInt( parts[0] ); 53 | this.destinationHostname = parts[1]; 54 | this.destinationPort = Integer.parseInt( parts[2] ); 55 | } 56 | else { 57 | this.localPort = 0; // dynamically assigned port 58 | this.destinationHostname = parts[0]; 59 | this.destinationPort = Integer.parseInt( parts[1] ); 60 | } 61 | } 62 | 63 | /** 64 | * Creates a Tunnel to destinationPort on 65 | * destinationHostname from a dynamically assigned port on 66 | * localhost. Simply calls 67 | * 68 | * @param destinationHostname 69 | * The hostname to tunnel to 70 | * @param destinationPort 71 | * The port to tunnel to 72 | * 73 | * @see #Tunnel(int, String, int) 74 | * @see rfc4254 75 | */ 76 | public Tunnel( String destinationHostname, int destinationPort ) { 77 | this( 0, destinationHostname, destinationPort ); 78 | } 79 | 80 | /** 81 | * Creates a Tunnel to destinationPort on 82 | * destinationHostname from localPort on 83 | * localhost. 84 | * 85 | * @param localPort 86 | * The local port to bind to 87 | * @param destinationHostname 88 | * The hostname to tunnel to 89 | * @param destinationPort 90 | * The port to tunnel to 91 | * 92 | * @see #Tunnel(String, int, String, int) 93 | * @see rfc4254 94 | */ 95 | public Tunnel( int localPort, String destinationHostname, int destinationPort ) { 96 | this( null, localPort, destinationHostname, destinationPort ); 97 | } 98 | 99 | /** 100 | * Creates a Tunnel to destinationPort on 101 | * destinationHostname from localPort on 102 | * localAlias. 103 | *

104 | * This is similar in behavior to the -L option in ssh, with 105 | * the exception that you can specify 0 for the local port in 106 | * which case the port will be dynamically allocated and you can 107 | * {@link #getAssignedLocalPort()} after the tunnel has been started. 108 | *

109 | *

110 | * A common use case for localAlias might be to link your 111 | * loopback interfaces to names via an entries in /etc/hosts 112 | * which would allow you to use the same port number for more than one 113 | * tunnel. For example: 114 | * 115 | *

116 |      * 127.0.0.2 foo
117 |      * 127.0.0.3 bar
118 |      * 
119 | * 120 | * Would allow you to have both of these open at the same time: 121 | * 122 | *
123 |      * new Tunnel( "foo", 1234, "remote_foo", 1234 );
124 |      * new Tunnel( "bar", 1234, "remote_bar", 1234 );
125 |      * 
126 | * 127 | * @param localAlias 128 | * The local interface to bind to 129 | * @param localPort 130 | * The local port to bind to 131 | * @param destinationHostname 132 | * The hostname to tunnel to 133 | * @param destinationPort 134 | * The port to tunnel to 135 | * 136 | * @see com.jcraft.jsch.Session#setPortForwardingL(String, int, String, int) 137 | * @see rfc4254 138 | */ 139 | public Tunnel( String localAlias, int localPort, String destinationHostname, int destinationPort ) { 140 | this.localAlias = localAlias; 141 | this.localPort = localPort; 142 | this.destinationHostname = destinationHostname; 143 | this.destinationPort = destinationPort; 144 | } 145 | 146 | /** 147 | * Returns true if other is a Tunnel whose spec 148 | * (either specified or calculated) is equal to this tunnels 149 | * spec. 150 | * 151 | * @return True if both tunnels have equivalent spec's 152 | * 153 | * @see #getSpec() 154 | */ 155 | @Override 156 | public boolean equals( Object other ) { 157 | return (other instanceof Tunnel) && 158 | getSpec().equals( ((Tunnel) other).getSpec() ); 159 | } 160 | 161 | /** 162 | * Returns the local port currently bound to. If 0 was 163 | * specified as the port to bind to, this will return the dynamically 164 | * allocated port, otherwise it will return the port specified. 165 | * 166 | * @return The local port currently bound to 167 | */ 168 | public int getAssignedLocalPort() { 169 | return assignedLocalPort == 0 ? localPort : assignedLocalPort; 170 | } 171 | 172 | /** 173 | * Returns the hostname of the destination. 174 | * 175 | * @return The hostname of the destination 176 | */ 177 | public String getDestinationHostname() { 178 | return destinationHostname; 179 | } 180 | 181 | /** 182 | * Returns the port of the destination. 183 | * 184 | * @return The port of the destination 185 | */ 186 | public int getDestinationPort() { 187 | return destinationPort; 188 | } 189 | 190 | /** 191 | * Returns the local alias bound to. See rfc4254 for 193 | * details on acceptible values. 194 | * 195 | * @return The local alias bound to 196 | */ 197 | public String getLocalAlias() { 198 | return localAlias; 199 | } 200 | 201 | /** 202 | * Returns the port this tunnel was configured with. If you want to get the 203 | * runtime port, use {@link #getAssignedLocalPort()}. 204 | * 205 | * @return The port this tunnel was configured with 206 | */ 207 | public int getLocalPort() { 208 | return localPort; 209 | } 210 | 211 | /** 212 | * Returns the spec string (either calculated or specified) for this tunnel. 213 | *

214 | * A spec string is composed of 4 parts separated by a colon (: 215 | * ): 216 | *

    217 | *
  1. localAlias (optional)
  2. 218 | *
  3. localPort (optional)
  4. 219 | *
  5. destinationHostname
  6. 220 | *
  7. destinationPort
  8. 221 | *
222 | * 223 | * @return The spec string 224 | */ 225 | public String getSpec() { 226 | if ( spec == null ) { 227 | spec = toString().toLowerCase(); 228 | } 229 | return spec; 230 | } 231 | 232 | @Override 233 | public int hashCode() { 234 | return getSpec().hashCode(); 235 | } 236 | 237 | void setAssignedLocalPort( int port ) { 238 | this.assignedLocalPort = port; 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return (localAlias == null ? "" : localAlias + ":") 244 | + (assignedLocalPort == 0 245 | ? localPort 246 | : ("(0)" + assignedLocalPort)) 247 | + ":" + destinationHostname + ":" + destinationPort; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/tunnel/TunnelConnection.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.util.Arrays; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | 16 | import com.jcraft.jsch.JSchException; 17 | import com.jcraft.jsch.Session; 18 | import com.pastdev.jsch.IOUtils; 19 | import com.pastdev.jsch.SessionFactory; 20 | 21 | 22 | /** 23 | * A TunnelConnection represents an ssh connection that opens one or more 24 | * {@link com.pastdev.jsch.tunnel.Tunnel Tunnel's}. 25 | */ 26 | public class TunnelConnection implements Closeable { 27 | private static Logger logger = LoggerFactory.getLogger( TunnelConnection.class ); 28 | 29 | private Map tunnelsByDestination; 30 | private Session session; 31 | private SessionFactory sessionFactory; 32 | private Iterable tunnels; 33 | 34 | /** 35 | * Creates a TunnelConnection using the the sessionFactory to 36 | * obtain its ssh connection with a single tunnel defined by 37 | * {@link com.pastdev.jsch.tunnel.Tunnel#Tunnel(int, String, int) 38 | * Tunnel(localPort, destinationHostname, destinationPort)}. 39 | * 40 | * @param sessionFactory 41 | * The sessionFactory 42 | * @param localPort 43 | * The local port to bind to 44 | * @param destinationHostname 45 | * The destination hostname to tunnel to 46 | * @param destinationPort 47 | * The destination port to tunnel to 48 | */ 49 | public TunnelConnection( SessionFactory sessionFactory, int localPort, String destinationHostname, int destinationPort ) { 50 | this( sessionFactory, new Tunnel( localPort, destinationHostname, destinationPort ) ); 51 | } 52 | 53 | /** 54 | * Creates a TunnelConnection using the the sessionFactory to 55 | * obtain its ssh connection with a list of 56 | * {@link com.pastdev.jsch.tunnel.Tunnel Tunnel's}. 57 | * 58 | * @param sessionFactory 59 | * The sessionFactory 60 | * @param tunnels 61 | * The tunnels 62 | */ 63 | public TunnelConnection( SessionFactory sessionFactory, Tunnel... tunnels ) { 64 | this( sessionFactory, Arrays.asList( tunnels ) ); 65 | } 66 | 67 | /** 68 | * Creates a TunnelConnection using the the sessionFactory to 69 | * obtain its ssh connection with a list of 70 | * {@link com.pastdev.jsch.tunnel.Tunnel Tunnel's}. 71 | * 72 | * @param sessionFactory 73 | * The sessionFactory 74 | * @param tunnels 75 | * The tunnels 76 | */ 77 | public TunnelConnection( SessionFactory sessionFactory, List tunnels ) { 78 | this.sessionFactory = sessionFactory; 79 | this.tunnels = tunnels; 80 | this.tunnelsByDestination = new HashMap(); 81 | 82 | for ( Tunnel tunnel : tunnels ) { 83 | tunnelsByDestination.put( hostnamePortKey( tunnel ), tunnel ); 84 | } 85 | } 86 | 87 | /** 88 | * Closes the underlying ssh session causing all tunnels to be closed. 89 | */ 90 | public void close() throws IOException { 91 | if ( session != null && session.isConnected() ) { 92 | session.disconnect(); 93 | } 94 | session = null; 95 | 96 | // unnecessary, but seems right to undo what we did 97 | for ( Tunnel tunnel : tunnels ) { 98 | tunnel.setAssignedLocalPort( 0 ); 99 | } 100 | } 101 | 102 | /** 103 | * Returns the tunnel matching the supplied values, or null if 104 | * there isn't one that matches. 105 | * 106 | * @param destinationHostname 107 | * The tunnels destination hostname 108 | * @param destinationPort 109 | * The tunnels destination port 110 | * 111 | * @return The tunnel matching the supplied values 112 | */ 113 | public Tunnel getTunnel( String destinationHostname, int destinationPort ) { 114 | return tunnelsByDestination.get( 115 | hostnamePortKey( destinationHostname, destinationPort ) ); 116 | } 117 | 118 | private String hostnamePortKey( Tunnel tunnel ) { 119 | return hostnamePortKey( tunnel.getDestinationHostname(), 120 | tunnel.getDestinationPort() ); 121 | } 122 | 123 | private String hostnamePortKey( String hostname, int port ) { 124 | return hostname + ":" + port; 125 | } 126 | 127 | /** 128 | * Returns true if the underlying ssh session is open. 129 | * 130 | * @return True if the underlying ssh session is open 131 | */ 132 | public boolean isOpen() { 133 | return session != null && session.isConnected(); 134 | } 135 | 136 | /** 137 | * Opens a session and connects all of the tunnels. 138 | * 139 | * @throws JSchException 140 | * If unable to connect 141 | */ 142 | public void open() throws JSchException { 143 | if ( isOpen() ) { 144 | return; 145 | } 146 | session = sessionFactory.newSession(); 147 | 148 | logger.debug( "connecting session" ); 149 | session.connect(); 150 | 151 | for ( Tunnel tunnel : tunnels ) { 152 | int assignedPort = 0; 153 | if ( tunnel.getLocalAlias() == null ) { 154 | assignedPort = session.setPortForwardingL( 155 | tunnel.getLocalPort(), 156 | tunnel.getDestinationHostname(), 157 | tunnel.getDestinationPort() ); 158 | } 159 | else { 160 | assignedPort = session.setPortForwardingL( 161 | tunnel.getLocalAlias(), 162 | tunnel.getLocalPort(), 163 | tunnel.getDestinationHostname(), 164 | tunnel.getDestinationPort() ); 165 | } 166 | tunnel.setAssignedLocalPort( assignedPort ); 167 | logger.debug( "added tunnel {}", tunnel ); 168 | } 169 | logger.info( "forwarding {}", this ); 170 | } 171 | 172 | /** 173 | * Closes, and re-opens the session and all its tunnels. Effectively calls 174 | * {@link #close()} followed by a call to {@link #open()}. 175 | * 176 | * @throws JSchException 177 | * If unable to connect 178 | */ 179 | public void reopen() throws JSchException { 180 | IOUtils.closeAndLogException( this ); 181 | open(); 182 | } 183 | 184 | @Override 185 | public String toString() { 186 | StringBuilder builder = new StringBuilder( sessionFactory.toString() ); 187 | for ( Tunnel tunnel : tunnels ) { 188 | builder.append( " -L " ).append( tunnel ); 189 | } 190 | return builder.toString(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/tunnel/TunnelConnectionManager.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | import java.io.BufferedReader; 5 | import java.io.Closeable; 6 | import java.io.File; 7 | import java.io.FileReader; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.HashMap; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import java.util.regex.Pattern; 17 | 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | 23 | import com.jcraft.jsch.JSchException; 24 | import com.pastdev.jsch.IOUtils; 25 | import com.pastdev.jsch.SessionFactory; 26 | import com.pastdev.jsch.SessionFactory.SessionFactoryBuilder; 27 | import com.pastdev.jsch.proxy.SshProxy; 28 | 29 | 30 | /** 31 | * Manages a collection of tunnels. This implementation will: 32 | *
    33 | *
  • Ensure a minimum number of ssh connections are made
  • 34 | *
  • Ensure all connections are open/closed at the same time
  • 35 | *
  • Provide a convenient syntax for defining tunnels
  • 36 | *
37 | */ 38 | public class TunnelConnectionManager implements Closeable { 39 | private static final Pattern PATTERN_TUNNELS_CFG_COMMENT_LINE = Pattern.compile( "^\\s*(?:#.*)?$" ); 40 | private static Logger logger = LoggerFactory.getLogger( TunnelConnectionManager.class ); 41 | 42 | private SessionFactory baseSessionFactory; 43 | private List tunnelConnections; 44 | 45 | /** 46 | * Creates a TunnelConnectionManager that will use the 47 | * baseSessionFactory to obtain its session connections. 48 | * Because this constructor does not set the tunnel connections for you, you 49 | * will need to call {@link #setTunnelConnections(Iterable)}. 50 | * 51 | * @param baseSessionFactory 52 | * The session factory 53 | * @throws JSchException 54 | * For connection failures 55 | * 56 | * @see #setTunnelConnections(Iterable) 57 | */ 58 | public TunnelConnectionManager( SessionFactory baseSessionFactory ) throws JSchException { 59 | logger.debug( "Creating TunnelConnectionManager" ); 60 | this.baseSessionFactory = baseSessionFactory; 61 | } 62 | 63 | /** 64 | * Creates a TunnelConnectionManager that will use the 65 | * baseSessionFactory to obtain its session connections and 66 | * provide the tunnels specified. 67 | * 68 | * @param baseSessionFactory 69 | * The session factory 70 | * @param pathAndSpecList 71 | * A list of {@link #setTunnelConnections(Iterable) path and 72 | * spec} strings 73 | * @throws JSchException 74 | * For connection failures 75 | * 76 | * @see #setTunnelConnections(Iterable) 77 | */ 78 | public TunnelConnectionManager( SessionFactory baseSessionFactory, String... pathAndSpecList ) throws JSchException { 79 | this( baseSessionFactory, Arrays.asList( pathAndSpecList ) ); 80 | } 81 | 82 | /** 83 | * Creates a TunnelConnectionManager that will use the 84 | * baseSessionFactory to obtain its session connections and 85 | * provide the tunnels specified. 86 | * 87 | * @param baseSessionFactory 88 | * The session factory 89 | * @param pathAndSpecList 90 | * A list of {@link #setTunnelConnections(Iterable) path and 91 | * spec} strings 92 | * @throws JSchException 93 | * For connection failures 94 | * 95 | * @see #setTunnelConnections(Iterable) 96 | */ 97 | public TunnelConnectionManager( SessionFactory baseSessionFactory, Iterable pathAndSpecList ) throws JSchException { 98 | this( baseSessionFactory ); 99 | setTunnelConnections( pathAndSpecList ); 100 | } 101 | 102 | /** 103 | * Closes all sessions and their associated tunnels. 104 | * 105 | * @see com.pastdev.jsch.tunnel.TunnelConnection#close() 106 | */ 107 | @Override 108 | public void close() { 109 | for ( TunnelConnection tunnelConnection : tunnelConnections ) { 110 | IOUtils.closeAndLogException( tunnelConnection ); 111 | } 112 | } 113 | 114 | /** 115 | * Will re-open any connections that are not still open. 116 | * 117 | * @throws JSchException 118 | * For connection failures 119 | */ 120 | public void ensureOpen() throws JSchException { 121 | for ( TunnelConnection tunnelConnection : tunnelConnections ) { 122 | if ( !tunnelConnection.isOpen() ) { 123 | tunnelConnection.reopen(); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Returns the tunnel matching the supplied values, or null if 130 | * there isn't one that matches. 131 | * 132 | * @param destinationHostname 133 | * The tunnels destination hostname 134 | * @param destinationPort 135 | * The tunnels destination port 136 | * 137 | * @return The tunnel matching the supplied values 138 | * 139 | * @see com.pastdev.jsch.tunnel.TunnelConnection#getTunnel(String, int) 140 | */ 141 | public Tunnel getTunnel( String destinationHostname, int destinationPort ) { 142 | // might be better to cache, but dont anticipate massive numbers 143 | // of tunnel connections... 144 | for ( TunnelConnection tunnelConnection : tunnelConnections ) { 145 | Tunnel tunnel = tunnelConnection.getTunnel( 146 | destinationHostname, destinationPort ); 147 | if ( tunnel != null ) { 148 | return tunnel; 149 | } 150 | } 151 | return null; 152 | } 153 | 154 | /** 155 | * Opens all the necessary sessions and connects all of the tunnels. 156 | * 157 | * @throws JSchException 158 | * For connection failures 159 | * 160 | * @see com.pastdev.jsch.tunnel.TunnelConnection#open() 161 | */ 162 | public void open() throws JSchException { 163 | for ( TunnelConnection tunnelConnection : tunnelConnections ) { 164 | tunnelConnection.open(); 165 | } 166 | } 167 | 168 | /** 169 | * Creates a set of tunnel connections based upon the contents of 170 | * tunnelsConfig. The format of this file is one path and 171 | * tunnel per line. Comments and empty lines are allowed and are excluded 172 | * using the pattern ^\s*(?:#.*)?$. 173 | * 174 | * @param tunnelsConfig A file containing tunnel configuration 175 | * @throws IOException 176 | * If unable to read from tunnelsConfig 177 | * @throws JSchException 178 | * For connection failures 179 | */ 180 | public void setTunnelConnectionsFromFile( File tunnelsConfig ) throws IOException, JSchException { 181 | List pathAndTunnels = new ArrayList(); 182 | BufferedReader reader = null; 183 | try { 184 | reader = new BufferedReader( new FileReader( tunnelsConfig ) ); 185 | String line = null; 186 | while ( (line = reader.readLine()) != null ) { 187 | if ( PATTERN_TUNNELS_CFG_COMMENT_LINE.matcher( line ).matches() ) { 188 | continue; 189 | } 190 | pathAndTunnels.add( line ); 191 | } 192 | } 193 | finally { 194 | if ( reader != null ) { 195 | IOUtils.closeAndLogException( reader ); 196 | } 197 | } 198 | 199 | setTunnelConnections( pathAndTunnels ); 200 | } 201 | 202 | /** 203 | * Creates a set of tunnel connections based upon the pathAndTunnels. Each 204 | * entry of pathAndTunnels must be of the form (in EBNF): 207 | * 208 | *
209 |      * path and tunnels = path and tunnel, {new line, path and tunnel}
210 |      * path and tunnel = path, "|", tunnel
211 |      * new line = "\n"
212 |      * path = path part, {"->", path part}
213 |      * path part = {user, "@"}, hostname
214 |      * tunnel = {local part}, ":", destination hostname, ":", destination port
215 |      * local part = {local alias, ":"}, local port
216 |      * local alias = hostname
217 |      * local port = port
218 |      * destination hostname = hostname
219 |      * destination port = port
220 |      * user = ? user name ?
221 |      * hostname = ? hostname ?
222 |      * port = ? port ?
223 |      * 
224 | * 225 | *

226 | * For example: 227 | *

228 | *

229 | * 230 | * jimhenson@admin.muppets.com->animal@drteethandtheelectricmahem.muppets.com|drteeth:8080:drteeth.muppets.com:80 231 | * 232 | *

233 | *

234 | * Says open an ssh connection as user jimhenson to host 235 | * admin.muppets.com. Then, through that connection, open a 236 | * connection as user animal to host 237 | * drteethandtheelectricmahem.muppets.com. Then map local port 238 | * 8080 on the interface with alias drteeth 239 | * through the two-hop tunnel to port 80 on 240 | * drteeth.muppets.com. 241 | *

242 | * 243 | * @param pathAndSpecList 244 | * A list of path and spec entries 245 | * 246 | * @throws JSchException 247 | * For connection failures 248 | */ 249 | public void setTunnelConnections( Iterable pathAndSpecList ) throws JSchException { 250 | Map> tunnelMap = new HashMap>(); 251 | for ( String pathAndSpecString : pathAndSpecList ) { 252 | String[] pathAndSpec = pathAndSpecString.trim().split( "\\|" ); 253 | Set tunnelList = tunnelMap.get( pathAndSpec[0] ); 254 | if ( tunnelList == null ) { 255 | tunnelList = new HashSet(); 256 | tunnelMap.put( pathAndSpec[0], tunnelList ); 257 | } 258 | tunnelList.add( new Tunnel( pathAndSpec[1] ) ); 259 | } 260 | 261 | tunnelConnections = new ArrayList(); 262 | SessionFactoryCache sessionFactoryCache = new SessionFactoryCache( baseSessionFactory ); 263 | for ( String path : tunnelMap.keySet() ) { 264 | tunnelConnections.add( new TunnelConnection( sessionFactoryCache.getSessionFactory( path ), 265 | new ArrayList( tunnelMap.get( path ) ) ) ); 266 | } 267 | } 268 | 269 | /* 270 | * Used to ensure duplicate paths are not created which will minimize the 271 | * number of connections needed. 272 | */ 273 | static class SessionFactoryCache { 274 | private Map sessionFactoryByPath; 275 | private SessionFactory defaultSessionFactory; 276 | 277 | SessionFactoryCache( SessionFactory baseSessionFactory ) { 278 | this.defaultSessionFactory = baseSessionFactory; 279 | this.sessionFactoryByPath = new HashMap(); 280 | } 281 | 282 | public SessionFactory getSessionFactory( String path ) throws JSchException { 283 | SessionFactory sessionFactory = null; 284 | String key = null; 285 | for ( String part : path.split( "\\-\\>" ) ) { 286 | if ( key == null ) { 287 | key = part; 288 | } 289 | else { 290 | key += "->" + part; 291 | } 292 | if ( sessionFactoryByPath.containsKey( key ) ) { 293 | sessionFactory = sessionFactoryByPath.get( key ); 294 | continue; 295 | } 296 | 297 | SessionFactoryBuilder builder = null; 298 | if ( sessionFactory == null ) { 299 | builder = defaultSessionFactory.newSessionFactoryBuilder(); 300 | } 301 | else { 302 | builder = sessionFactory.newSessionFactoryBuilder() 303 | .setProxy( new SshProxy( sessionFactory ) ); 304 | } 305 | 306 | // start with [username@]hostname[:port] 307 | String[] userAtHost = part.split( "\\@" ); 308 | String hostname = null; 309 | if ( userAtHost.length == 2 ) { 310 | builder.setUsername( userAtHost[0] ); 311 | hostname = userAtHost[1]; 312 | } 313 | else { 314 | hostname = userAtHost[0]; 315 | } 316 | 317 | // left with hostname[:port] 318 | String[] hostColonPort = hostname.split( "\\:" ); 319 | builder.setHostname( hostColonPort[0] ); 320 | if ( hostColonPort.length == 2 ) { 321 | builder.setPort( Integer.parseInt( hostColonPort[1] ) ); 322 | } 323 | 324 | sessionFactory = builder.build(); 325 | } 326 | return sessionFactory; 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/com/pastdev/jsch/tunnel/TunneledDataSourceWrapper.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.io.PrintWriter; 7 | import java.sql.Connection; 8 | import java.sql.SQLException; 9 | import java.sql.SQLFeatureNotSupportedException; 10 | 11 | 12 | import javax.sql.DataSource; 13 | 14 | 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | 19 | /** 20 | * Wraps a {@link javax.sql.DataSource DataSource}, adding the ability to 21 | * tunnel over ssh for the connection. 22 | */ 23 | public class TunneledDataSourceWrapper implements DataSource, Closeable { 24 | private static Logger log = LoggerFactory.getLogger( TunneledDataSourceWrapper.class ); 25 | private DataSource dataSource; 26 | private TunnelConnectionManager tunnel; 27 | 28 | /** 29 | * Creates a DataSource that will ensure all of the tunnels specified by 30 | * tunnel are opened when datasource connections are created, 31 | * and closed when the datasource is closed. 32 | * 33 | * @param tunnel The tunnel manager 34 | * @param dataSource The datasource that requires tunneled connections 35 | * 36 | * @see com.pastdev.jsch.tunnel.TunnelConnectionManager 37 | */ 38 | public TunneledDataSourceWrapper( TunnelConnectionManager tunnel, DataSource dataSource ) { 39 | this.tunnel = tunnel; 40 | this.dataSource = dataSource; 41 | } 42 | 43 | @Override 44 | public void close() throws IOException { 45 | log.info( "closing tunnel" ); 46 | tunnel.close(); 47 | } 48 | 49 | private void ensureTunnelIsOpen() throws SQLException { 50 | try { 51 | tunnel.ensureOpen(); 52 | } 53 | catch ( Exception e ) { 54 | throw new SQLException( "unable to open tunnel", e ); 55 | } 56 | } 57 | 58 | @Override 59 | public Connection getConnection() throws SQLException { 60 | ensureTunnelIsOpen(); 61 | return dataSource.getConnection(); 62 | } 63 | 64 | @Override 65 | public Connection getConnection( String username, String password ) throws SQLException { 66 | ensureTunnelIsOpen(); 67 | return dataSource.getConnection( username, password ); 68 | } 69 | 70 | @Override 71 | public int getLoginTimeout() throws SQLException { 72 | return dataSource.getLoginTimeout(); 73 | } 74 | 75 | @Override 76 | public PrintWriter getLogWriter() throws SQLException { 77 | return dataSource.getLogWriter(); 78 | } 79 | 80 | @Override 81 | public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { 82 | return dataSource.getParentLogger(); 83 | } 84 | 85 | @Override 86 | public boolean isWrapperFor( Class iface ) throws SQLException { 87 | if ( dataSource.getClass().equals( iface.getClass() ) ) return true; 88 | return dataSource.isWrapperFor( iface ); 89 | } 90 | 91 | @Override 92 | public void setLoginTimeout( int seconds ) throws SQLException { 93 | dataSource.setLoginTimeout( seconds ); 94 | } 95 | 96 | @Override 97 | public void setLogWriter( PrintWriter out ) throws SQLException { 98 | dataSource.setLogWriter( out ); 99 | } 100 | 101 | @SuppressWarnings( "unchecked" ) 102 | @Override 103 | public T unwrap( Class iface ) throws SQLException { 104 | if ( dataSource.getClass().equals( iface.getClass() ) ) return (T) dataSource; 105 | return dataSource.unwrap( iface ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/ConnectionTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.fail; 6 | 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.Properties; 11 | 12 | 13 | import org.junit.Assume; 14 | import org.junit.BeforeClass; 15 | import org.junit.Ignore; 16 | import org.junit.Test; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | 21 | import com.jcraft.jsch.JSchException; 22 | import com.jcraft.jsch.Session; 23 | import com.jcraft.jsch.UIKeyboardInteractive; 24 | import com.jcraft.jsch.UserInfo; 25 | 26 | 27 | public class ConnectionTest { 28 | private static Logger logger = LoggerFactory.getLogger( ConnectionTest.class ); 29 | private static String hostname; 30 | private static int port; 31 | private static String username; 32 | private static String correctPassword; 33 | private static String incorrectPassword; 34 | 35 | @BeforeClass 36 | public static void initializeClass() { 37 | InputStream inputStream = null; 38 | Properties properties = null; 39 | try { 40 | inputStream = ClassLoader.getSystemResourceAsStream( "configuration.properties" ); 41 | Assume.assumeNotNull( inputStream ); 42 | properties = new Properties(); 43 | properties.load( inputStream ); 44 | } 45 | catch ( IOException e ) { 46 | logger.warn( "cant find properties file (tests will be skipped): {}", e.getMessage() ); 47 | Assume.assumeNoException( e ); 48 | } 49 | finally { 50 | IOUtils.closeAndLogException( inputStream ); 51 | } 52 | 53 | username = properties.getProperty( "scp.out.test.username" ); 54 | hostname = properties.getProperty( "scp.out.test.host" ); 55 | correctPassword = properties.getProperty( "scp.out.test.password" ); 56 | port = Integer.parseInt( properties.getProperty( "scp.out.test.port" ) ); 57 | 58 | incorrectPassword = correctPassword + "."; 59 | } 60 | 61 | private SessionFactory getKeyboardInteractiveAuthenticatingSessionFactory( String password ) 62 | throws IOException { 63 | final DefaultSessionFactory defaultSessionFactory = 64 | new DefaultSessionFactory( username, hostname, port ); 65 | defaultSessionFactory.setConfig( "PreferredAuthentications", 66 | "keyboard-interactive" ); 67 | defaultSessionFactory.setUserInfo( new TestUserInfo( password ) ); 68 | return defaultSessionFactory; 69 | } 70 | 71 | private SessionFactory getPasswordAuthenticatingSessionFactory( String password ) 72 | throws IOException { 73 | final DefaultSessionFactory defaultSessionFactory = 74 | new DefaultSessionFactory( username, hostname, port ); 75 | defaultSessionFactory.setConfig( "PreferredAuthentications", "password" ); 76 | defaultSessionFactory.setPassword( password ); 77 | return defaultSessionFactory; 78 | } 79 | 80 | private void testKeyboardInteractiveConnectionWithPassword( String password ) throws Exception { 81 | Session session = null; 82 | try { 83 | session = getKeyboardInteractiveAuthenticatingSessionFactory( password ) 84 | .newSession(); 85 | session.connect(); 86 | } 87 | finally { 88 | if ( session != null ) { 89 | session.disconnect(); 90 | } 91 | } 92 | } 93 | 94 | private void testPasswordConnectionWithPassword( String password ) throws Exception { 95 | Session session = null; 96 | try { 97 | session = getPasswordAuthenticatingSessionFactory( password ) 98 | .newSession(); 99 | session.connect(); 100 | } 101 | finally { 102 | if ( session != null ) { 103 | session.disconnect(); 104 | } 105 | } 106 | } 107 | 108 | @Ignore 109 | @Test 110 | public void testKeyboardInteractiveConnectionWithCorrectPassword() { 111 | // Doesnt seem to work with cygwin 112 | Assume.assumeNotNull( username, correctPassword ); 113 | try { 114 | testKeyboardInteractiveConnectionWithPassword( correctPassword ); 115 | } 116 | catch ( Exception e ) { 117 | fail( e.getMessage() ); 118 | } 119 | } 120 | 121 | @Ignore 122 | @Test 123 | public void testKeyboardInteractiveConnectionWithIncorrectPassword() { 124 | // Doesnt seem to work with cygwin 125 | Assume.assumeNotNull( username, incorrectPassword ); 126 | try { 127 | testKeyboardInteractiveConnectionWithPassword( incorrectPassword ); 128 | } 129 | catch ( JSchException e ) { 130 | assertEquals( "Auth fail", e.getMessage() ); 131 | } 132 | catch ( Exception e ) { 133 | fail( "Unexpected exception: " + e.getMessage() ); 134 | } 135 | } 136 | 137 | @Test 138 | public void testPasswordConnectionWithCorrectPassword() { 139 | Assume.assumeNotNull( username, correctPassword ); 140 | try { 141 | testPasswordConnectionWithPassword( correctPassword ); 142 | } 143 | catch ( Exception e ) { 144 | fail( e.getMessage() ); 145 | } 146 | } 147 | 148 | @Test 149 | public void testPasswordConnectionWithIncorrectPassword() { 150 | Assume.assumeNotNull( username, incorrectPassword ); 151 | try { 152 | testPasswordConnectionWithPassword( incorrectPassword ); 153 | } 154 | catch ( JSchException e ) { 155 | assertEquals( "Auth fail", e.getMessage() ); 156 | } 157 | catch ( Exception e ) { 158 | fail( "Unexpected exception: " + e.getMessage() ); 159 | } 160 | } 161 | 162 | private static final class TestUserInfo implements UserInfo, UIKeyboardInteractive { 163 | private String password; 164 | 165 | public TestUserInfo( String password ) { 166 | this.password = password; 167 | } 168 | 169 | @Override 170 | public String[] promptKeyboardInteractive( String destination, String name, String instruction, String[] prompt, boolean[] echo ) { 171 | logger.debug( "getPassphrase()" ); 172 | return new String[] { password }; 173 | } 174 | 175 | @Override 176 | public String getPassphrase() { 177 | logger.debug( "getPassphrase()" ); 178 | return null; 179 | } 180 | 181 | @Override 182 | public String getPassword() { 183 | logger.debug( "getPassword()" ); 184 | return password; 185 | } 186 | 187 | @Override 188 | public boolean promptPassword( String message ) { 189 | logger.debug( "promptPassword({})", message ); 190 | return false; 191 | } 192 | 193 | @Override 194 | public boolean promptPassphrase( String message ) { 195 | logger.debug( "promptPassphrase({})", message ); 196 | return false; 197 | } 198 | 199 | @Override 200 | public boolean promptYesNo( String message ) { 201 | logger.debug( "promptYesNo({})", message ); 202 | return false; 203 | } 204 | 205 | @Override 206 | public void showMessage( String message ) { 207 | logger.debug( "showMessage({})", message ); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/UriTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | 6 | 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | 10 | 11 | import org.junit.Test; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | 16 | public class UriTest { 17 | public static Logger logger = LoggerFactory.getLogger( UriTest.class ); 18 | 19 | @Test 20 | public void testUri() throws URISyntaxException { 21 | URI uri1 = new URI( "ssh.unix://ltheisen@localhost:22/root/path/" ); 22 | logger.debug( "uri1 is '{}'", uri1 ); 23 | URI uri2 = uri1.resolve( "relative/part" ); 24 | logger.debug( "uri2 is '{}'", uri2 ); 25 | URI uri3 = uri2.resolve( "/new/root/" ); 26 | logger.debug( "uri3 is '{}'", uri3 ); 27 | 28 | assertEquals( "ltheisen", uri1.getUserInfo() ); 29 | assertEquals( "localhost", uri1.getHost() ); 30 | assertEquals( 22, uri1.getPort() ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/command/CommandRunnerTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.command; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.fail; 6 | 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.nio.charset.Charset; 11 | import java.util.Properties; 12 | 13 | 14 | import org.junit.Assume; 15 | import org.junit.BeforeClass; 16 | import org.junit.Test; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | 21 | import com.jcraft.jsch.JSchException; 22 | import com.pastdev.jsch.DefaultSessionFactory; 23 | import com.pastdev.jsch.IOUtils; 24 | import com.pastdev.jsch.SessionFactory; 25 | import com.pastdev.jsch.command.CommandRunner.ExecuteResult; 26 | import com.pastdev.jsch.proxy.SshProxyTest; 27 | 28 | 29 | public class CommandRunnerTest { 30 | private static Logger logger = LoggerFactory.getLogger( SshProxyTest.class ); 31 | protected static final Charset UTF8 = Charset.forName( "UTF-8" ); 32 | protected static SessionFactory sessionFactory; 33 | protected static Properties properties; 34 | 35 | private String expected = "there is absolutely no chance this is gonna work!"; 36 | 37 | @BeforeClass 38 | public static void initializeClass() { 39 | InputStream inputStream = null; 40 | try { 41 | inputStream = ClassLoader.getSystemResourceAsStream( "configuration.properties" ); 42 | Assume.assumeNotNull( inputStream ); 43 | properties = new Properties(); 44 | properties.load( inputStream ); 45 | } 46 | catch ( IOException e ) { 47 | logger.warn( "cant find properties file (tests will be skipped): {}", e.getMessage() ); 48 | logger.debug( "cant find properties file:", e ); 49 | properties = null; 50 | return; 51 | } 52 | finally { 53 | if ( inputStream != null ) { 54 | try { 55 | inputStream.close(); 56 | } 57 | catch ( IOException e ) { 58 | // really, i dont care... 59 | } 60 | } 61 | } 62 | 63 | String knownHosts = properties.getProperty( "ssh.knownHosts" ); 64 | String privateKey = properties.getProperty( "ssh.privateKey" ); 65 | String username = properties.getProperty( "scp.out.test.username" ); 66 | String hostname = "localhost"; 67 | int port = Integer.parseInt( properties.getProperty( "scp.out.test.port" ) ); 68 | 69 | DefaultSessionFactory defaultSessionFactory = new DefaultSessionFactory( username, hostname, port ); 70 | try { 71 | defaultSessionFactory.setKnownHosts( knownHosts ); 72 | defaultSessionFactory.setIdentityFromPrivateKey( privateKey ); 73 | } 74 | catch ( JSchException e ) { 75 | Assume.assumeNoException( e ); 76 | } 77 | sessionFactory = defaultSessionFactory; 78 | } 79 | 80 | @Test 81 | public void testCommandRunner() { 82 | CommandRunner commandRunner = null; 83 | try { 84 | commandRunner = new CommandRunner( sessionFactory ); 85 | 86 | logger.debug( "run a command" ); 87 | ExecuteResult result = commandRunner.execute( "echo " + expected ); 88 | assertEquals( 0, result.getExitCode() ); 89 | assertEquals( expected + "\n", result.getStdout() ); 90 | assertEquals( "", result.getStderr() ); 91 | 92 | // test automatic reconnect... 93 | commandRunner.close(); 94 | 95 | logger.debug( "now try a second command" ); 96 | result = commandRunner.execute( "echo second " + expected ); 97 | assertEquals( 0, result.getExitCode() ); 98 | assertEquals( "second " + expected + "\n", result.getStdout() ); 99 | assertEquals( "", result.getStderr() ); 100 | 101 | logger.debug( "and a third command" ); 102 | result = commandRunner.execute( "echo third " + expected ); 103 | assertEquals( 0, result.getExitCode() ); 104 | assertEquals( "third " + expected + "\n", result.getStdout() ); 105 | assertEquals( "", result.getStderr() ); 106 | 107 | logger.debug( "wow, they all worked" ); 108 | } 109 | catch ( Exception e ) { 110 | logger.error( "failed for command runner {}: {}", commandRunner, e ); 111 | logger.debug( "failed:", e ); 112 | fail( e.getMessage() ); 113 | } 114 | finally { 115 | IOUtils.closeAndLogException( commandRunner ); 116 | } 117 | } 118 | 119 | @Test 120 | public void testSlowCommand() { 121 | CommandRunner commandRunner = null; 122 | try { 123 | commandRunner = new CommandRunner( sessionFactory ); 124 | 125 | logger.debug( "run a command" ); 126 | ExecuteResult result = commandRunner.execute( "sleep 3;echo " + expected ); 127 | assertEquals( 0, result.getExitCode() ); 128 | assertEquals( expected + "\n", result.getStdout() ); 129 | assertEquals( "", result.getStderr() ); 130 | 131 | // test automatic reconnect... 132 | commandRunner.close(); 133 | 134 | logger.debug( "cool, even slow commands work" ); 135 | } 136 | catch ( Exception e ) { 137 | logger.error( "failed for command runner {}: {}", commandRunner, e ); 138 | logger.debug( "failed:", e ); 139 | fail( e.getMessage() ); 140 | } 141 | finally { 142 | IOUtils.closeAndLogException( commandRunner ); 143 | } 144 | } 145 | 146 | @Test 147 | public void testDetectOs() { 148 | CommandRunner commandRunner = null; 149 | try { 150 | commandRunner = new CommandRunner( sessionFactory ); 151 | 152 | logger.debug( "run a command" ); 153 | ExecuteResult result = commandRunner.execute( "ver" ); 154 | if ( result.getExitCode() == 0 ) { 155 | logger.debug( "likely windows: " + result.getStdout() ); 156 | } 157 | else { 158 | result = commandRunner.execute( "uname -a" ); 159 | if ( result.getExitCode() == 0 ) { 160 | logger.debug( "likely unix: " + result.getStdout() ); 161 | } 162 | else { 163 | logger.debug( "unknown os: " + result.getStdout() ); 164 | } 165 | } 166 | logger.debug( "wow, they all worked" ); 167 | } 168 | catch ( Exception e ) { 169 | logger.error( "failed for command runner {}: {}", commandRunner, e ); 170 | logger.debug( "failed:", e ); 171 | fail( e.getMessage() ); 172 | } 173 | finally { 174 | IOUtils.closeAndLogException( commandRunner ); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/proxy/SshProxyTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.proxy; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.fail; 6 | 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.nio.charset.Charset; 11 | import java.util.Properties; 12 | 13 | 14 | import org.junit.Assume; 15 | import org.junit.BeforeClass; 16 | import org.junit.Test; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | 21 | import com.jcraft.jsch.Channel; 22 | import com.jcraft.jsch.ChannelExec; 23 | import com.jcraft.jsch.JSchException; 24 | import com.jcraft.jsch.Proxy; 25 | import com.jcraft.jsch.Session; 26 | import com.pastdev.jsch.DefaultSessionFactory; 27 | import com.pastdev.jsch.IOUtils; 28 | import com.pastdev.jsch.SessionFactory; 29 | 30 | 31 | public class SshProxyTest { 32 | private static Logger logger = LoggerFactory.getLogger( SshProxyTest.class ); 33 | protected static final Charset UTF8 = Charset.forName( "UTF-8" ); 34 | protected static DefaultSessionFactory sessionFactory; 35 | protected static Properties properties; 36 | protected static String username; 37 | protected static String hostname; 38 | protected static int port; 39 | 40 | private String expected = "there is absolutely no chance this is gonna work!"; 41 | 42 | @BeforeClass 43 | public static void initializeClass() { 44 | InputStream inputStream = null; 45 | try { 46 | inputStream = ClassLoader.getSystemResourceAsStream( "configuration.properties" ); 47 | Assume.assumeNotNull( inputStream ); 48 | properties = new Properties(); 49 | properties.load( inputStream ); 50 | } 51 | catch ( IOException e ) { 52 | logger.warn( "cant find properties file (tests will be skipped): {}", e.getMessage() ); 53 | logger.debug( "cant find properties file:", e ); 54 | properties = null; 55 | return; 56 | } 57 | finally { 58 | if ( inputStream != null ) { 59 | try { 60 | inputStream.close(); 61 | } 62 | catch ( IOException e ) { 63 | // really, i dont care... 64 | } 65 | } 66 | } 67 | 68 | String knownHosts = properties.getProperty( "ssh.knownHosts" ); 69 | String privateKey = properties.getProperty( "ssh.privateKey" ); 70 | username = properties.getProperty( "scp.out.test.username" ); 71 | hostname = "localhost"; 72 | port = Integer.parseInt( properties.getProperty( "scp.out.test.port" ) ); 73 | 74 | sessionFactory = new DefaultSessionFactory( username, hostname, port ); 75 | try { 76 | sessionFactory.setKnownHosts( knownHosts ); 77 | sessionFactory.setIdentityFromPrivateKey( privateKey ); 78 | } 79 | catch ( JSchException e ) { 80 | Assume.assumeNoException( e ); 81 | } 82 | } 83 | 84 | @Test 85 | public void testSshProxy() { 86 | Proxy proxy = null; 87 | Session session = null; 88 | Channel channel = null; 89 | try { 90 | SessionFactory proxySessionFactory = sessionFactory.newSessionFactoryBuilder() 91 | .setHostname( "localhost" ).setPort( SessionFactory.SSH_PORT ).build(); 92 | SessionFactory destinationSessionFactory = sessionFactory.newSessionFactoryBuilder() 93 | .setProxy( new SshProxy( proxySessionFactory ) ).build(); 94 | session = destinationSessionFactory.newSession(); 95 | 96 | session.connect(); 97 | 98 | channel = session.openChannel( "exec" ); 99 | ((ChannelExec)channel).setCommand( "echo " + expected ); 100 | InputStream inputStream = channel.getInputStream(); 101 | channel.connect(); 102 | 103 | // echo adds \n 104 | assertEquals( expected + "\n", IOUtils.copyToString( inputStream, UTF8 ) ); 105 | } 106 | catch ( Exception e ) { 107 | logger.error( "failed for proxy {}: {}", proxy, e ); 108 | logger.debug( "failed:", e ); 109 | fail( e.getMessage() ); 110 | } 111 | finally { 112 | if ( channel != null && channel.isConnected() ) { 113 | channel.disconnect(); 114 | } 115 | if ( session != null && session.isConnected() ) { 116 | session.disconnect(); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/scp/ScpFileTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.assertTrue; 6 | import static org.junit.Assert.fail; 7 | 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.UUID; 12 | 13 | 14 | import org.junit.After; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | 21 | import com.jcraft.jsch.Session; 22 | import com.pastdev.jsch.IOUtils; 23 | 24 | 25 | public class ScpFileTest extends ScpTestBase { 26 | private static Logger logger = LoggerFactory.getLogger( ScpFileTest.class ); 27 | 28 | private File dir; 29 | private String expected = "Oh well, lets just use a different string..."; 30 | private File file; 31 | private String filename; 32 | private String rootDir; 33 | 34 | @After 35 | public void after() { 36 | IOUtils.deleteFiles( file, dir ); 37 | } 38 | 39 | @Before 40 | public void before() { 41 | rootDir = UUID.randomUUID().toString(); 42 | 43 | dir = new File( filesystemPath, rootDir ); 44 | assertTrue( dir.mkdirs() ); 45 | logger.debug( "{} created dir {}", dir.exists() ? "succesfully" : "failed to", dir ); 46 | filename = UUID.randomUUID().toString() + ".txt"; 47 | file = new File( dir, filename ); 48 | } 49 | 50 | @Test 51 | public void testCopyFromFile() { 52 | String toFilename = "actual.txt"; 53 | File toFile = new File( dir, toFilename ); 54 | try { 55 | IOUtils.writeFile( file, expected, UTF8 ); 56 | ScpFile to = new ScpFile( sessionFactory, scpPath, rootDir, toFilename ); 57 | to.copyFrom( file ); 58 | String actual = IOUtils.readFile( toFile ); 59 | assertEquals( expected, actual ); 60 | } 61 | catch ( Exception e ) { 62 | logger.error( "failed for {}: {}", filename, e ); 63 | logger.debug( "failed:", e ); 64 | fail( e.getMessage() ); 65 | } 66 | finally { 67 | IOUtils.deleteFiles( toFile ); 68 | } 69 | } 70 | 71 | @Test 72 | public void testCopyToFile() { 73 | String fromFilename = "expected.txt"; 74 | File fromFile = new File( dir, fromFilename ); 75 | try { 76 | IOUtils.writeFile( fromFile, expected, UTF8 ); 77 | ScpFile from = new ScpFile( sessionFactory, scpPath, rootDir, fromFilename ); 78 | from.copyTo( file ); 79 | String actual = IOUtils.readFile( file, UTF8 ); 80 | assertEquals( expected, actual ); 81 | } 82 | catch ( Exception e ) { 83 | logger.error( "failed for {}: {}", filename, e ); 84 | logger.debug( "failed:", e ); 85 | fail( e.getMessage() ); 86 | } 87 | finally { 88 | IOUtils.deleteFiles( fromFile ); 89 | } 90 | } 91 | 92 | @Test 93 | public void testCopyToScpFile() { 94 | String fromFilename = "expected.txt"; 95 | File fromFile = new File( dir, fromFilename ); 96 | Session toSession = null; 97 | try { 98 | IOUtils.writeFile( fromFile, expected, UTF8 ); 99 | ScpFile from = new ScpFile( sessionFactory, scpPath, rootDir, fromFilename ); 100 | ScpFile to = new ScpFile( sessionFactory, scpPath, rootDir, filename ); 101 | from.copyTo( to ); 102 | String actual = IOUtils.readFile( file, UTF8 ); 103 | assertEquals( expected, actual ); 104 | } 105 | catch ( Exception e ) { 106 | logger.error( "failed for {}: {}", filename, e ); 107 | logger.debug( "failed:", e ); 108 | fail( e.getMessage() ); 109 | } 110 | finally { 111 | if ( toSession != null && toSession.isConnected() ) { 112 | toSession.disconnect(); 113 | } 114 | IOUtils.deleteFiles( fromFile ); 115 | } 116 | } 117 | 118 | @Test 119 | public void testGetInputStream() { 120 | try { 121 | ScpFileInputStream scpFileInputStream = null; 122 | try { 123 | IOUtils.writeFile( file, expected ); 124 | ScpFile scpFile = new ScpFile( sessionFactory, scpPath, rootDir, filename ); 125 | scpFileInputStream = scpFile.getInputStream(); 126 | String actual = IOUtils.copyToString( scpFileInputStream, UTF8 ); 127 | 128 | assertEquals( expected, actual ); 129 | } 130 | finally { 131 | if ( scpFileInputStream != null ) { 132 | try { 133 | scpFileInputStream.close(); 134 | } 135 | catch ( IOException e ) { 136 | logger.error( "failed to close ScpInputStream for {}: {}", filename, e ); 137 | logger.debug( "failed to close ScpInputStream:", e ); 138 | } 139 | } 140 | } 141 | } 142 | catch ( Exception e ) { 143 | logger.error( "failed for {}: {}", filename, e ); 144 | logger.debug( "failed:", e ); 145 | fail( e.getMessage() ); 146 | } 147 | } 148 | 149 | @Test 150 | public void testGetOutputStream() { 151 | try { 152 | ScpFileOutputStream outputStream = null; 153 | try { 154 | ScpFile scpFile = new ScpFile( sessionFactory, scpPath, rootDir, filename ); 155 | outputStream = scpFile.getOutputStream( expected.length() ); 156 | IOUtils.copyFromString( expected, UTF8, outputStream ); 157 | } 158 | finally { 159 | if ( outputStream != null ) { 160 | try { 161 | outputStream.close(); 162 | } 163 | catch ( IOException e ) { 164 | logger.error( "failed to close ScpOutputStream for {}: {}", filename, e ); 165 | logger.debug( "failed to close ScpOutputStream:", e ); 166 | } 167 | } 168 | } 169 | 170 | String actual = IOUtils.readFile( file, UTF8 ); 171 | assertEquals( expected, actual ); 172 | } 173 | catch ( Exception e ) { 174 | logger.error( "failed for {}: {}", filename, e ); 175 | logger.debug( "failed:", e ); 176 | fail( e.getMessage() ); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/scp/ScpStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.fail; 6 | 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.UUID; 15 | 16 | 17 | import org.junit.After; 18 | import org.junit.Assert; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | 25 | import com.jcraft.jsch.Session; 26 | import com.pastdev.jsch.IOUtils; 27 | import com.pastdev.jsch.scp.CopyMode; 28 | import com.pastdev.jsch.scp.ScpEntry; 29 | import com.pastdev.jsch.scp.ScpInputStream; 30 | import com.pastdev.jsch.scp.ScpOutputStream; 31 | 32 | 33 | public class ScpStreamTest extends ScpTestBase { 34 | private static Logger logger = LoggerFactory.getLogger( ScpStreamTest.class ); 35 | 36 | private File dir1; 37 | private String dir1Name; 38 | private File dir2; 39 | private String dir2Name; 40 | private String expected1 = "This is a test"; 41 | private String expected2 = "This is only a test"; 42 | private String expected3 = "Of the Emergency Broadcast System"; 43 | private File file1; 44 | private String file1Name; 45 | private File file2; 46 | private String file2Name; 47 | private File file3; 48 | private String file3Name; 49 | private Session session; 50 | 51 | @After 52 | public void after() { 53 | if ( session != null && session.isConnected() ) { 54 | session.disconnect(); 55 | } 56 | 57 | IOUtils.deleteFiles( file3, file2, file1, dir2, dir1 ); 58 | } 59 | 60 | @Before 61 | public void before() { 62 | dir1Name = UUID.randomUUID().toString(); 63 | dir2Name = "dir"; 64 | 65 | dir1 = new File( filesystemPath, dir1Name ); 66 | dir2 = new File( dir1, dir2Name ); 67 | assertTrue( dir2.mkdirs() ); 68 | logger.debug( "{} created dir {}", dir2.exists() ? "succesfully" : "failed to", dir2 ); 69 | 70 | file1Name = "file1.txt"; 71 | file1 = new File( dir1, file1Name ); 72 | file2Name = "file2.txt"; 73 | file2 = new File( dir2, file2Name ); 74 | file3Name = "file3.txt"; 75 | file3 = new File( dir1, file3Name ); 76 | 77 | try { 78 | session = sessionFactory.newSession(); 79 | } 80 | catch ( Exception e ) { 81 | logger.error( "failed to initialize session from factory {}: {}", sessionFactory, e ); 82 | logger.debug( "failed:", e ); 83 | fail( e.getMessage() ); 84 | } 85 | } 86 | 87 | private String joinPath( List dirs, String file ) { 88 | int fileIndex = dirs.size(); 89 | String[] parts = dirs.toArray( new String[fileIndex + 1] ); 90 | parts[fileIndex] = file; 91 | return joinPath( parts ); 92 | } 93 | 94 | private String joinPath( String... parts ) { 95 | StringBuilder builder = new StringBuilder(); 96 | for ( int i = 0; i < parts.length; i++ ) { 97 | if ( i > 0 ) { 98 | builder.append( "/" ); 99 | } 100 | builder.append( parts[i] ); 101 | } 102 | return builder.toString(); 103 | } 104 | 105 | @Test 106 | public void testInputStream() { 107 | try { 108 | IOUtils.writeFile( file1, expected1, UTF8 ); 109 | IOUtils.writeFile( file2, expected2, UTF8 ); 110 | IOUtils.writeFile( file3, expected3, UTF8 ); 111 | } 112 | catch ( IOException e ) { 113 | Assert.fail( e.getMessage() ); 114 | } 115 | 116 | ScpInputStream inputStream = null; 117 | try { 118 | inputStream = new ScpInputStream( sessionFactory, joinPath( scpPath, dir1Name, "*" ), CopyMode.RECURSIVE ); 119 | Map fileNameToContents = new HashMap(); 120 | List dirs = new ArrayList(); 121 | while ( true ) { 122 | ScpEntry entry = inputStream.getNextEntry(); 123 | if ( entry == null ) break; 124 | if ( entry.isDirectory() ) { 125 | dirs.add( entry.getName() ); 126 | } 127 | else if ( entry.isEndOfDirectory() ) { 128 | dirs.remove( dirs.size() - 1 ); 129 | } 130 | if ( entry.isFile() ) { 131 | String path = joinPath( dirs, entry.getName() ); 132 | String data = IOUtils.copyToString( inputStream ); 133 | fileNameToContents.put( path, data ); 134 | } 135 | } 136 | 137 | Assert.assertEquals( expected1, fileNameToContents.get( file1Name ) ); 138 | Assert.assertEquals( expected2, fileNameToContents.get( joinPath( dir2Name, file2Name ) ) ); 139 | Assert.assertEquals( expected3, fileNameToContents.get( file3Name ) ); 140 | } 141 | catch ( Exception e ) { 142 | logger.error( "failed to write to ScpInputStream: {}", e.getMessage() ); 143 | logger.debug( "failed to write to ScpInputStream: ", e ); 144 | Assert.fail( e.getMessage() ); 145 | } 146 | finally { 147 | IOUtils.closeAndLogException( inputStream ); 148 | } 149 | } 150 | 151 | @Test 152 | public void testOutputStream() { 153 | ScpOutputStream outputStream = null; 154 | try { 155 | outputStream = new ScpOutputStream( sessionFactory, joinPath( scpPath, dir1Name ), CopyMode.RECURSIVE ); 156 | 157 | outputStream.putNextEntry( file1Name, expected1.length() ); 158 | outputStream.write( expected1.getBytes( UTF8 ) ); 159 | outputStream.closeEntry(); 160 | 161 | outputStream.putNextEntry( dir2Name ); 162 | 163 | outputStream.putNextEntry( file2Name, expected2.length() ); 164 | outputStream.write( expected2.getBytes( UTF8 ) ); 165 | outputStream.closeEntry(); 166 | 167 | // instead of outputStream.closeEntry() lets try this: 168 | outputStream.putNextEntry( ScpEntry.newEndOfDirectory() ); 169 | 170 | outputStream.putNextEntry( file3Name, expected3.length() ); 171 | outputStream.write( expected3.getBytes( UTF8 ) ); 172 | outputStream.closeEntry(); 173 | } 174 | catch ( Exception e ) { 175 | logger.error( "failed to write to ScpOutputStream: {}", e.getMessage() ); 176 | logger.debug( "failed to write to ScpOutputStream: ", e ); 177 | Assert.fail( e.getMessage() ); 178 | } 179 | finally { 180 | IOUtils.closeAndLogException( outputStream ); 181 | } 182 | 183 | try { 184 | Assert.assertTrue( file1.exists() ); 185 | Assert.assertEquals( expected1, IOUtils.readFile( file1, UTF8 ) ); 186 | Assert.assertTrue( file2.exists() ); 187 | Assert.assertEquals( expected2, IOUtils.readFile( file2, UTF8 ) ); 188 | Assert.assertTrue( file3.exists() ); 189 | Assert.assertEquals( expected3, IOUtils.readFile( file3, UTF8 ) ); 190 | } 191 | catch ( Exception e ) { 192 | logger.error( "failed to read file contents: {}", e.getMessage() ); 193 | logger.debug( "failed to read file contents: ", e ); 194 | Assert.fail( e.getMessage() ); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/scp/ScpTestBase.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.scp; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.charset.Charset; 7 | import java.util.Properties; 8 | 9 | 10 | import org.junit.Assume; 11 | import org.junit.Before; 12 | import org.junit.BeforeClass; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | 17 | import com.jcraft.jsch.JSchException; 18 | import com.pastdev.jsch.DefaultSessionFactory; 19 | import com.pastdev.jsch.SessionFactory; 20 | 21 | 22 | public class ScpTestBase { 23 | private static Logger logger = LoggerFactory.getLogger( ScpTestBase.class ); 24 | 25 | protected static final Charset UTF8 = Charset.forName( "UTF-8" ); 26 | protected static SessionFactory sessionFactory; 27 | protected static Properties properties; 28 | protected static String scpPath; 29 | protected static String filesystemPath; 30 | 31 | @BeforeClass 32 | public static void initializeClass() { 33 | InputStream inputStream = null; 34 | try { 35 | inputStream = ClassLoader.getSystemResourceAsStream( "configuration.properties" ); 36 | Assume.assumeNotNull( inputStream ); 37 | properties = new Properties(); 38 | properties.load( inputStream ); 39 | } 40 | catch ( IOException e ) { 41 | logger.warn( "cant find properties file (tests will be skipped): {}", e.getMessage() ); 42 | logger.debug( "cant find properties file:", e ); 43 | properties = null; 44 | return; 45 | } 46 | finally { 47 | if ( inputStream != null ) { 48 | try { 49 | inputStream.close(); 50 | } 51 | catch ( IOException e ) { 52 | // really, i dont care... 53 | } 54 | } 55 | } 56 | 57 | String knownHosts = properties.getProperty( "ssh.knownHosts" ); 58 | String privateKey = properties.getProperty( "ssh.privateKey" ); 59 | scpPath = properties.getProperty( "scp.out.test.scpPath" ); 60 | filesystemPath = properties.getProperty( "scp.out.test.filesystemPath" ); 61 | String username = properties.getProperty( "scp.out.test.username" ); 62 | String hostname = "localhost"; 63 | int port = Integer.parseInt( properties.getProperty( "scp.out.test.port" ) ); 64 | 65 | DefaultSessionFactory defaultSessionFactory = new DefaultSessionFactory( username, hostname, port ); 66 | try { 67 | defaultSessionFactory.setKnownHosts( knownHosts ); 68 | defaultSessionFactory.setIdentityFromPrivateKey( privateKey ); 69 | } 70 | catch ( JSchException e ) { 71 | Assume.assumeNoException( e ); 72 | } 73 | sessionFactory = defaultSessionFactory; 74 | } 75 | 76 | @Before 77 | public void beforeTest() { 78 | Assume.assumeNotNull( properties ); // skip tests if properties not set 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/tunnel/TunnelConnectionManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.assertNotNull; 6 | import static org.junit.Assert.fail; 7 | 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | 13 | import org.junit.Test; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | 18 | import com.jcraft.jsch.JSchException; 19 | import com.pastdev.jsch.DefaultSessionFactory; 20 | 21 | 22 | public class TunnelConnectionManagerTest { 23 | private static Logger logger = LoggerFactory.getLogger( TunnelConnectionManagerTest.class ); 24 | 25 | @Test 26 | public void testGetTunnel() { 27 | TunnelConnectionManager manager = null; 28 | try { 29 | manager = new TunnelConnectionManager( 30 | new DefaultSessionFactory() ); 31 | } 32 | catch ( JSchException e ) { 33 | logger.debug( "unable to create TunnelConnectionManager: ", e ); 34 | fail( "unable to create TunnelConnectionManager: " + e.getMessage() ); 35 | } 36 | assertNotNull( manager ); 37 | 38 | List pathAndSpecList = new ArrayList(); 39 | pathAndSpecList.add( "joe@crabshack|crab:10:imitationcrab:20" ); 40 | pathAndSpecList.add( "bob@redlobster|lobster:15:tail:20" ); 41 | try { 42 | manager.setTunnelConnections( pathAndSpecList ); 43 | } 44 | catch ( JSchException e ) { 45 | logger.debug( "unable to set pathAndSpecList: ", e ); 46 | fail( "unable to setPathAndSpecList: " + e.getMessage() ); 47 | } 48 | 49 | Tunnel tunnel = manager.getTunnel( "imitationcrab", 20 ); 50 | assertEquals( "crab", tunnel.getLocalAlias() ); 51 | assertEquals( 10, tunnel.getLocalPort() ); 52 | tunnel = manager.getTunnel( "tail", 20 ); 53 | assertEquals( "lobster", tunnel.getLocalAlias() ); 54 | assertEquals( 15, tunnel.getLocalPort() ); 55 | } 56 | } -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/tunnel/TunnelConnectionTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.fail; 6 | 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.net.ServerSocket; 12 | import java.net.Socket; 13 | import java.nio.charset.Charset; 14 | import java.util.Properties; 15 | import java.util.concurrent.locks.Condition; 16 | import java.util.concurrent.locks.ReentrantLock; 17 | 18 | 19 | import org.junit.After; 20 | import org.junit.Assume; 21 | import org.junit.Before; 22 | import org.junit.BeforeClass; 23 | import org.junit.Test; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | 28 | import com.jcraft.jsch.JSchException; 29 | import com.pastdev.jsch.DefaultSessionFactory; 30 | import com.pastdev.jsch.IOUtils; 31 | import com.pastdev.jsch.SessionFactory; 32 | 33 | 34 | public class TunnelConnectionTest { 35 | private static Logger logger = LoggerFactory.getLogger( TunnelConnectionTest.class ); 36 | private static final Charset UTF8 = Charset.forName( "UTF-8" ); 37 | private static SessionFactory sessionFactory; 38 | private static Properties properties; 39 | 40 | private String expected = "This will be amazing if it works"; 41 | private StringBuffer serviceBuffer; 42 | private int servicePort = 59703; 43 | private Thread serviceThread; 44 | 45 | private final ReentrantLock serviceLock = new ReentrantLock(); 46 | private final Condition serviceBufferReady = serviceLock.newCondition(); 47 | private final Condition serviceConnectAccepted = serviceLock.newCondition(); 48 | private final Condition serviceConnected = serviceLock.newCondition(); 49 | private final Condition serviceReady = serviceLock.newCondition(); 50 | private final Condition serviceWrittenTo = serviceLock.newCondition(); 51 | 52 | @BeforeClass 53 | public static void initializeClass() { 54 | InputStream inputStream = null; 55 | try { 56 | inputStream = ClassLoader.getSystemResourceAsStream( "configuration.properties" ); 57 | Assume.assumeNotNull( inputStream ); 58 | properties = new Properties(); 59 | properties.load( inputStream ); 60 | } 61 | catch ( IOException e ) { 62 | logger.warn( "cant find properties file (tests will be skipped): {}", e.getMessage() ); 63 | logger.debug( "cant find properties file:", e ); 64 | properties = null; 65 | return; 66 | } 67 | finally { 68 | if ( inputStream != null ) { 69 | try { 70 | inputStream.close(); 71 | } 72 | catch ( IOException e ) { 73 | // really, i dont care... 74 | } 75 | } 76 | } 77 | 78 | String knownHosts = properties.getProperty( "ssh.knownHosts" ); 79 | String privateKey = properties.getProperty( "ssh.privateKey" ); 80 | String username = properties.getProperty( "scp.out.test.username" ); 81 | String hostname = "localhost"; 82 | int port = Integer.parseInt( properties.getProperty( "scp.out.test.port" ) ); 83 | 84 | DefaultSessionFactory defaultSessionFactory = new DefaultSessionFactory( username, hostname, port ); 85 | try { 86 | defaultSessionFactory.setKnownHosts( knownHosts ); 87 | defaultSessionFactory.setIdentityFromPrivateKey( privateKey ); 88 | } 89 | catch ( JSchException e ) { 90 | logger.error( "Failed to configure default session, skipping tests: {}", e.getMessage() ); 91 | logger.debug( "Failed to configure default session, skipping tests:", e ); 92 | Assume.assumeNoException( e ); 93 | } 94 | sessionFactory = defaultSessionFactory; 95 | } 96 | 97 | @After 98 | public void afterTest() throws InterruptedException { 99 | serviceThread.join(); 100 | } 101 | 102 | @Before 103 | public void beforeTest() throws InterruptedException { 104 | Assume.assumeNotNull( properties ); // skip tests if properties not set 105 | serviceBuffer = new StringBuffer(); 106 | serviceThread = new Thread( new Runnable() { 107 | public void run() { 108 | ServerSocket serverSocket = null; 109 | InputStream inputStream = null; 110 | 111 | serviceLock.lock(); 112 | try { 113 | serverSocket = new ServerSocket( servicePort ); 114 | logger.debug( "opening service on port {}", servicePort ); 115 | serviceReady.signalAll(); 116 | serviceConnected.await(); 117 | logger.trace( "waiting for connection" ); 118 | inputStream = serverSocket.accept().getInputStream(); 119 | serviceConnectAccepted.signalAll(); 120 | logger.trace( "connected, now wait for write" ); 121 | serviceWrittenTo.await(); 122 | logger.trace( "accepted connection, now read data" ); 123 | String data = IOUtils.copyToString( inputStream, UTF8 ); 124 | logger.trace( "read {}", data ); 125 | serviceBuffer.append( data ); 126 | serviceBufferReady.signalAll(); 127 | } 128 | catch ( Exception e ) { 129 | logger.error( "failed to open service on port {}: ", servicePort, e ); 130 | logger.debug( "failed:", e ); 131 | } 132 | finally { 133 | serviceLock.unlock(); 134 | 135 | logger.debug( "closing down service on port {}", servicePort ); 136 | IOUtils.closeAndLogException( inputStream ); 137 | IOUtils.closeAndLogException( serverSocket ); 138 | } 139 | } 140 | } ); 141 | logger.debug( "starting service" ); 142 | serviceThread.start(); 143 | 144 | serviceLock.lock(); 145 | try { 146 | logger.trace( "wait for serviceReady" ); 147 | serviceReady.await(); // wait for service to open socket 148 | logger.trace( "service is ready now!" ); 149 | } 150 | finally { 151 | serviceLock.unlock(); 152 | } 153 | } 154 | 155 | @Test 156 | public void testService() { 157 | assertEquals( expected, writeToService( servicePort, expected ) ); 158 | } 159 | 160 | @Test 161 | public void testConnection() { 162 | final int tunnelPort1 = 59701; 163 | TunnelConnection tunnelConnection = null; 164 | try { 165 | tunnelConnection = new TunnelConnection( sessionFactory, 166 | new Tunnel( tunnelPort1, "localhost", servicePort ) ); 167 | tunnelConnection.open(); 168 | 169 | assertEquals( expected, writeToService( tunnelPort1, expected ) ); 170 | } 171 | catch ( Exception e ) { 172 | logger.error( "failed for {}: {}", tunnelConnection, e ); 173 | logger.debug( "failed:", e ); 174 | fail( e.getMessage() ); 175 | } 176 | finally { 177 | logger.debug( "close" ); 178 | IOUtils.closeAndLogException( tunnelConnection ); 179 | } 180 | } 181 | 182 | @Test 183 | public void testDynamicPortConnection() { 184 | TunnelConnection tunnelConnection = null; 185 | try { 186 | String hostname = "localhost"; 187 | tunnelConnection = new TunnelConnection( sessionFactory, 188 | new Tunnel( hostname, servicePort ) ); 189 | tunnelConnection.open(); 190 | 191 | assertEquals( expected, writeToService( 192 | tunnelConnection.getTunnel( hostname, servicePort ) 193 | .getAssignedLocalPort(), expected ) ); 194 | } 195 | catch ( Exception e ) { 196 | logger.error( "failed for {}: {}", tunnelConnection, e ); 197 | logger.debug( "failed:", e ); 198 | fail( e.getMessage() ); 199 | } 200 | finally { 201 | logger.debug( "close" ); 202 | IOUtils.closeAndLogException( tunnelConnection ); 203 | } 204 | } 205 | 206 | private String writeToService( int port, String data ) { 207 | Socket socket = null; 208 | serviceLock.lock(); 209 | try { 210 | logger.debug( "connecting to service through port: {}", port ); 211 | socket = new Socket( "localhost", port ); 212 | logger.trace( "connected" ); 213 | serviceConnected.signalAll(); 214 | serviceConnectAccepted.await(); 215 | logger.trace( "now write to service" ); 216 | 217 | OutputStream outputStream = null; 218 | try { 219 | outputStream = socket.getOutputStream(); 220 | IOUtils.copyFromString( expected, UTF8, outputStream ); 221 | } 222 | finally { 223 | IOUtils.closeAndLogException( outputStream ); 224 | } 225 | 226 | logger.trace( "service written to" ); 227 | serviceWrittenTo.signalAll(); 228 | logger.trace( "wait for serviceBuffer" ); 229 | serviceBufferReady.await(); // wait for buffer to be updated 230 | return serviceBuffer.toString(); 231 | } 232 | catch ( Exception e ) { 233 | logger.error( "failed for service on port {}: {}", servicePort, e ); 234 | logger.debug( "failed:", e ); 235 | fail( e.getMessage() ); 236 | return null; // to make compiler happy 237 | } 238 | finally { 239 | serviceLock.unlock(); 240 | 241 | logger.debug( "close" ); 242 | IOUtils.closeAndLogException( socket ); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/test/java/com/pastdev/jsch/tunnel/TunneledDataSourceWrapperTest.java: -------------------------------------------------------------------------------- 1 | package com.pastdev.jsch.tunnel; 2 | 3 | 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.fail; 6 | import static org.junit.Assume.assumeNoException; 7 | 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.sql.DatabaseMetaData; 12 | import java.sql.SQLException; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Properties; 16 | 17 | 18 | import org.apache.tomcat.jdbc.pool.DataSource; 19 | import org.junit.Assume; 20 | import org.junit.BeforeClass; 21 | import org.junit.Test; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | 26 | import com.jcraft.jsch.JSchException; 27 | import com.pastdev.jsch.DefaultSessionFactory; 28 | import com.pastdev.jsch.IOUtils; 29 | 30 | 31 | public class TunneledDataSourceWrapperTest { 32 | private static Logger logger = LoggerFactory.getLogger( TunneledDataSourceWrapperTest.class ); 33 | private static Properties properties; 34 | private static DefaultSessionFactory sessionFactory; 35 | 36 | @BeforeClass 37 | public static void initialize() { 38 | InputStream inputStream = null; 39 | try { 40 | inputStream = ClassLoader.getSystemResourceAsStream( "configuration.properties" ); 41 | Assume.assumeNotNull( inputStream ); 42 | properties = new Properties(); 43 | properties.load( inputStream ); 44 | } 45 | catch ( IOException e ) { 46 | logger.warn( "cant find properties file (tests will be skipped): {}", e.getMessage() ); 47 | logger.debug( "cant find properties file:", e ); 48 | properties = null; 49 | return; 50 | } 51 | finally { 52 | if ( inputStream != null ) { 53 | try { 54 | inputStream.close(); 55 | } 56 | catch ( IOException e ) { 57 | // really, i dont care... 58 | } 59 | } 60 | } 61 | 62 | try { 63 | DataSource dataSource = new DataSource(); 64 | dataSource.setDriverClassName( properties.getProperty( "dataSource.driver" ) ); 65 | dataSource.setPassword( properties.getProperty( "dataSource.password" ) ); 66 | dataSource.setUsername( properties.getProperty( "dataSource.username" ) ); 67 | dataSource.setUrl( properties.getProperty( "dataSource.url" ) ); 68 | 69 | dataSource.getConnection().close(); 70 | 71 | dataSource.close( true ); 72 | logger.info( "got a connection, so we can assume the configuration is correct..." ); 73 | } 74 | catch ( Exception e ) { 75 | logger.warn( "skipping tests as no connection is available: {}", e.getMessage() ); 76 | logger.debug( "skipping tests as no connection is available: {}", e ); 77 | assumeNoException( e ); 78 | } 79 | 80 | String knownHosts = properties.getProperty( "ssh.knownHosts" ); 81 | String privateKey = properties.getProperty( "ssh.privateKey" ); 82 | 83 | sessionFactory = new DefaultSessionFactory(); 84 | try { 85 | sessionFactory.setKnownHosts( knownHosts ); 86 | sessionFactory.setIdentityFromPrivateKey( privateKey ); 87 | } 88 | catch ( JSchException e ) { 89 | Assume.assumeNoException( e ); 90 | } 91 | } 92 | 93 | @Test 94 | public void testTunneledDataSourceConnection() throws JSchException, IOException { 95 | assertTrue( true ); 96 | DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); 97 | dataSource.setDriverClassName( properties.getProperty( "dataSource.driver" ) ); 98 | dataSource.setPassword( properties.getProperty( "dataSource.password" ) ); 99 | dataSource.setUsername( properties.getProperty( "dataSource.username" ) ); 100 | dataSource.setUrl( properties.getProperty( "dataSource.tunnel.url" ) ); 101 | List pathAndSpecList = new ArrayList(); 102 | pathAndSpecList.add( properties.getProperty( "dataSource.tunnel.pathAndSpec" ) ); 103 | 104 | TunneledDataSourceWrapper wrapper = new TunneledDataSourceWrapper( 105 | new TunnelConnectionManager( 106 | sessionFactory, 107 | pathAndSpecList ), 108 | dataSource ); 109 | 110 | try { 111 | DatabaseMetaData metaData = wrapper.getConnection().getMetaData(); 112 | logger.debug( "{} {}", metaData.getDatabaseProductName(), metaData.getDatabaseProductVersion() ); 113 | } 114 | catch ( SQLException e ) { 115 | fail( "failed to obtain metadata: " + e.getMessage() ); 116 | } 117 | finally { 118 | IOUtils.closeAndLogException( wrapper ); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/resources/.gitignore: -------------------------------------------------------------------------------- 1 | /configuration.properties 2 | -------------------------------------------------------------------------------- /src/test/resources/configuration.properties_SAMPLE: -------------------------------------------------------------------------------- 1 | ssh.knownHosts=C:/Users/ltheisen/.ssh/known_hosts 2 | ssh.privateKey=C:/Users/ltheisen/.ssh/id_dsa 3 | scp.out.test.scpPath=/tmp 4 | scp.out.test.filesystemPath=C:/Cygwin/tmp 5 | scp.out.test.username=ltheisen 6 | scp.out.test.password= 7 | scp.out.test.port=22 8 | dataSource.driver=com.mysql.jdbc.Driver 9 | dataSource.password= 10 | dataSource.username= 11 | dataSource.url=jdbc:mysql://localhost:3306 12 | dataSource.tunnel.pathAndSpec=ltheisen@localhost|54354:localhost:3306 13 | dataSource.tunnel.url=jdbc:mysql://localhost:54354 -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------