├── .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 |
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 | *
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( MapsetIdentit(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 Listnull
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 Mapconfig
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( Maphostname
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 | * 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 Stack89 | * 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 | *
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 | *
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 | *
localAlias
(optional)localPort
(optional)destinationHostname
destinationPort
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, Listnull
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 | * 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, Iterablenull
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 | List209 | * 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 | *
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 | *
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