com.helger.jsch.tunnel.TunnelConnectionManager Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2016-2024 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.jsch.tunnel;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsHashSet;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsSet;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.stream.NonBlockingBufferedReader;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.string.StringHelper;
import com.helger.jsch.proxy.SshProxy;
import com.helger.jsch.session.AbstractSessionFactoryBuilder;
import com.helger.jsch.session.ISessionFactory;
import com.jcraft.jsch.JSchException;
/**
* Manages a collection of tunnels. This implementation will:
*
* - Ensure a minimum number of ssh connections are made
* - Ensure all connections are open/closed at the same time
* - Provide a convenient syntax for defining tunnels
*
*/
public class TunnelConnectionManager implements Closeable
{
private static final Pattern PATTERN_TUNNELS_CFG_COMMENT_LINE = Pattern.compile ("^\\s*(?:#.*)?$");
private static final Logger LOGGER = LoggerFactory.getLogger (TunnelConnectionManager.class);
private final ISessionFactory m_aBaseSessionFactory;
private ICommonsList m_aTunnelConnections;
/**
* Creates a TunnelConnectionManager that will use the
* baseSessionFactory
to obtain its session connections. Because
* this constructor does not set the tunnel connections for you, you will need
* to call {@link #setTunnelConnections(Iterable)}.
*
* @param baseSessionFactory
* The session factory
* @throws JSchException
* For connection failures
* @see #setTunnelConnections(Iterable)
*/
public TunnelConnectionManager (final ISessionFactory baseSessionFactory) throws JSchException
{
if (LOGGER.isDebugEnabled ())
LOGGER.debug ("Creating TunnelConnectionManager");
m_aBaseSessionFactory = baseSessionFactory;
}
/**
* Creates a TunnelConnectionManager that will use the
* baseSessionFactory
to obtain its session connections and
* provide the tunnels specified.
*
* @param baseSessionFactory
* The session factory
* @param pathAndSpecList
* A list of {@link #setTunnelConnections(Iterable) path and spec}
* strings
* @throws JSchException
* For connection failures
* @see #setTunnelConnections(Iterable)
*/
public TunnelConnectionManager (final ISessionFactory baseSessionFactory, final String... pathAndSpecList) throws JSchException
{
this (baseSessionFactory, Arrays.asList (pathAndSpecList));
}
/**
* Creates a TunnelConnectionManager that will use the
* baseSessionFactory
to obtain its session connections and
* provide the tunnels specified.
*
* @param baseSessionFactory
* The session factory
* @param pathAndSpecList
* A list of {@link #setTunnelConnections(Iterable) path and spec}
* strings
* @throws JSchException
* For connection failures
* @see #setTunnelConnections(Iterable)
*/
public TunnelConnectionManager (final ISessionFactory baseSessionFactory, final Iterable pathAndSpecList) throws JSchException
{
this (baseSessionFactory);
setTunnelConnections (pathAndSpecList);
}
/**
* Closes all sessions and their associated tunnels.
*
* @see com.helger.jsch.tunnel.TunnelConnection#close()
*/
@Override
public void close ()
{
for (final TunnelConnection tunnelConnection : m_aTunnelConnections)
StreamHelper.close (tunnelConnection);
}
/**
* Will re-open any connections that are not still open.
*
* @throws JSchException
* For connection failures
*/
public void ensureOpen () throws JSchException
{
for (final TunnelConnection tunnelConnection : m_aTunnelConnections)
if (!tunnelConnection.isOpen ())
tunnelConnection.reopen ();
}
/**
* Returns the tunnel matching the supplied values, or null
if
* there isn't one that matches.
*
* @param destinationHostname
* The tunnels destination hostname
* @param destinationPort
* The tunnels destination port
* @return The tunnel matching the supplied values
* @see com.helger.jsch.tunnel.TunnelConnection#getTunnel(String, int)
*/
public Tunnel getTunnel (final String destinationHostname, final int destinationPort)
{
// might be better to cache, but dont anticipate massive numbers
// of tunnel connections...
for (final TunnelConnection tunnelConnection : m_aTunnelConnections)
{
final Tunnel tunnel = tunnelConnection.getTunnel (destinationHostname, destinationPort);
if (tunnel != null)
return tunnel;
}
return null;
}
/**
* Opens all the necessary sessions and connects all of the tunnels.
*
* @throws JSchException
* For connection failures
* @see com.helger.jsch.tunnel.TunnelConnection#open()
*/
public void open () throws JSchException
{
for (final TunnelConnection tunnelConnection : m_aTunnelConnections)
tunnelConnection.open ();
}
/**
* Creates a set of tunnel connections based upon the contents of
* tunnelsConfig
. The format of this file is one path and tunnel
* per line. Comments and empty lines are allowed and are excluded using the
* pattern ^\s*(?:#.*)?$
.
*
* @param tunnelsConfig
* A file containing tunnel configuration
* @param aCharset
* Charset to use. May not be null
.
* @throws IOException
* If unable to read from tunnelsConfig
* @throws JSchException
* For connection failures
*/
public void setTunnelConnectionsFromFile (final File tunnelsConfig, @Nonnull final Charset aCharset) throws IOException, JSchException
{
final List aLines = new ArrayList <> ();
try (final NonBlockingBufferedReader reader = new NonBlockingBufferedReader (FileHelper.getReader (tunnelsConfig, aCharset)))
{
String sLine;
while ((sLine = reader.readLine ()) != null)
{
if (PATTERN_TUNNELS_CFG_COMMENT_LINE.matcher (sLine).matches ())
continue;
aLines.add (sLine);
}
}
setTunnelConnections (aLines);
}
/**
* Creates a set of tunnel connections based upon the pathAndTunnels. Each
* entry of pathAndTunnels must be of the form (in
* EBNF):
*
*
* path and tunnels = path and tunnel, {new line, path and tunnel}
* path and tunnel = path, "|", tunnel
* new line = "\n"
* path = path part, {"->", path part}
* path part = {user, "@"}, hostname
* tunnel = {local part}, ":", destination hostname, ":", destination port
* local part = {local alias, ":"}, local port
* local alias = hostname
* local port = port
* destination hostname = hostname
* destination port = port
* user = ? user name ?
* hostname = ? hostname ?
* port = ? port ?
*
*
* For example:
*
*
*
* [email protected]>[email protected]|drteeth:8080:drteeth.muppets.com:80
*
*
*
* Says open an ssh connection as user jimhenson
to host
* admin.muppets.com
. Then, through that connection, open a
* connection as user animal
to host
* drteethandtheelectricmahem.muppets.com
. Then map local port
* 8080
on the interface with alias drteeth
through
* the two-hop tunnel to port 80
on
* drteeth.muppets.com
.
*
*
* @param aPathAndSpecList
* A list of path and spec entries
* @throws JSchException
* For connection failures
*/
public void setTunnelConnections (@Nonnull final Iterable aPathAndSpecList) throws JSchException
{
final Map > aMap = new HashMap <> ();
for (final String sItem : aPathAndSpecList)
{
final String [] aPathAndSpec = StringHelper.getExplodedArray ('|', sItem.trim (), 2);
aMap.computeIfAbsent (aPathAndSpec[0], k -> new CommonsHashSet <> ()).add (new Tunnel (aPathAndSpec[1]));
}
m_aTunnelConnections = new CommonsArrayList <> ();
final SessionFactoryCache sessionFactoryCache = new SessionFactoryCache (m_aBaseSessionFactory);
for (final Map.Entry > aEntry : aMap.entrySet ())
{
final String path = aEntry.getKey ();
m_aTunnelConnections.add (new TunnelConnection (sessionFactoryCache.getSessionFactory (path), aEntry.getValue ().getCopyAsList ()));
}
}
/*
* Used to ensure duplicate paths are not created which will minimize the
* number of connections needed.
*/
static class SessionFactoryCache
{
private final Map sessionFactoryByPath;
private final ISessionFactory defaultSessionFactory;
SessionFactoryCache (final ISessionFactory baseSessionFactory)
{
this.defaultSessionFactory = baseSessionFactory;
this.sessionFactoryByPath = new HashMap <> ();
}
public ISessionFactory getSessionFactory (final String path) throws JSchException
{
ISessionFactory sessionFactory = null;
final StringBuilder key = new StringBuilder ();
for (final String part : StringHelper.getExploded ("->", path))
{
if (key.length () > 0)
key.append ("->");
key.append (part);
final String sKey = key.toString ();
if (sessionFactoryByPath.containsKey (sKey))
return sessionFactoryByPath.get (sKey);
final AbstractSessionFactoryBuilder builder;
if (sessionFactory == null)
builder = defaultSessionFactory.newSessionFactoryBuilder ();
else
builder = sessionFactory.newSessionFactoryBuilder ().setProxy (new SshProxy (sessionFactory));
// start with [username@]hostname[:port]
final String [] aUserAtHost = StringHelper.getExplodedArray ('@', part, 2);
final String sHostname;
if (aUserAtHost.length == 2)
{
builder.setUsername (aUserAtHost[0]);
sHostname = aUserAtHost[1];
}
else
{
sHostname = aUserAtHost[0];
}
// left with hostname[:port]
final String [] hostColonPort = StringHelper.getExplodedArray (':', sHostname, 2);
builder.setHostname (hostColonPort[0]);
if (hostColonPort.length == 2)
{
builder.setPort (Integer.parseInt (hostColonPort[1]));
}
sessionFactory = builder.build ();
}
return sessionFactory;
}
}
}