diff --git a/pom.xml b/pom.xml index ad23793..387cc0f 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ 1.6 - + maven-assembly-plugin @@ -39,10 +39,15 @@ + + org.apache.mina + mina-core + 2.0.9 + org.apache.sshd sshd-core - 0.12.0 + 0.14.0 org.slf4j @@ -69,6 +74,11 @@ commons-io 2.4 + + org.json + json + 20140107 + diff --git a/reverse-tunnel.conf.example b/reverse-tunnel.conf.example index e20405c..7b73fca 100644 --- a/reverse-tunnel.conf.example +++ b/reverse-tunnel.conf.example @@ -3,3 +3,4 @@ tunnel_host=0.0.0.0 http_port=2223 external_port_range=20000:30000 host_key_path=hostkey.ser +idle_token_timeout=86400 \ No newline at end of file diff --git a/src/main/java/org/fogbowcloud/ssh/Main.java b/src/main/java/org/fogbowcloud/ssh/Main.java index 5b0d67b..4374066 100644 --- a/src/main/java/org/fogbowcloud/ssh/Main.java +++ b/src/main/java/org/fogbowcloud/ssh/Main.java @@ -17,6 +17,11 @@ public static void main(String[] args) throws IOException { String externalPortRange = properties.getProperty("external_port_range"); String[] externalRangeSplit = externalPortRange.split(":"); String externalHostKeyPath = properties.getProperty("host_key_path"); + String idleTokenTimeoutStr = properties.getProperty("idle_token_timeout"); + Long idleTokenTimeout = null; + if (idleTokenTimeoutStr != null) { + idleTokenTimeout = Long.parseLong(idleTokenTimeoutStr) * 1000; + } TunnelHttpServer tunnelHttpServer = new TunnelHttpServer( Integer.parseInt(httpPort), @@ -24,6 +29,7 @@ public static void main(String[] args) throws IOException { Integer.parseInt(tunnelPort), Integer.parseInt(externalRangeSplit[0]), Integer.parseInt(externalRangeSplit[1]), + idleTokenTimeout, externalHostKeyPath); tunnelHttpServer.start(); } diff --git a/src/main/java/org/fogbowcloud/ssh/ReverseTunnelForwarder.java b/src/main/java/org/fogbowcloud/ssh/ReverseTunnelForwarder.java index 56022a9..5b9e484 100644 --- a/src/main/java/org/fogbowcloud/ssh/ReverseTunnelForwarder.java +++ b/src/main/java/org/fogbowcloud/ssh/ReverseTunnelForwarder.java @@ -85,8 +85,10 @@ public synchronized SshdSocketAddress localPortForwardingRequested(SshdSocketAdd public synchronized void localPortForwardingCancelled(SshdSocketAddress local) throws IOException { if (localForwards.remove(local) && acceptor != null) { - Nio2Acceptor a = (Nio2Acceptor) acceptor; - a.doCloseImmediately(); + if (acceptor instanceof Nio2Acceptor) { + Nio2Acceptor a = (Nio2Acceptor) acceptor; + a.doCloseImmediately(); + } acceptor.unbind(local.toInetSocketAddress()); if (acceptor.getBoundAddresses().isEmpty()) { acceptor.close(true); @@ -195,4 +197,16 @@ private SshdSocketAddress doBind(SshdSocketAddress address) throws IOException { } } + @Override + public SshdSocketAddress startDynamicPortForwarding(SshdSocketAddress arg0) + throws IOException { + return arg0; + } + + @Override + public void stopDynamicPortForwarding(SshdSocketAddress arg0) + throws IOException { + + } + } diff --git a/src/main/java/org/fogbowcloud/ssh/ReverseTunnelSession.java b/src/main/java/org/fogbowcloud/ssh/ReverseTunnelSession.java index 680b0ba..e82dbae 100644 --- a/src/main/java/org/fogbowcloud/ssh/ReverseTunnelSession.java +++ b/src/main/java/org/fogbowcloud/ssh/ReverseTunnelSession.java @@ -1,5 +1,6 @@ package org.fogbowcloud.ssh; +import org.apache.sshd.common.Service; import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.util.Buffer; import org.apache.sshd.server.ServerFactoryManager; @@ -17,5 +18,9 @@ protected void doHandleMessage(Buffer buffer) throws Exception { super.doHandleMessage(buffer); resetIdleTimeout(); } + + public Service getService() { + return currentService; + } } diff --git a/src/main/java/org/fogbowcloud/ssh/ReverseTunnelTcpipChannel.java b/src/main/java/org/fogbowcloud/ssh/ReverseTunnelTcpipChannel.java index 63895ce..2a19826 100644 --- a/src/main/java/org/fogbowcloud/ssh/ReverseTunnelTcpipChannel.java +++ b/src/main/java/org/fogbowcloud/ssh/ReverseTunnelTcpipChannel.java @@ -13,7 +13,6 @@ import org.apache.sshd.common.channel.ChannelOutputStream; import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.util.Buffer; -import org.apache.sshd.common.util.CloseableUtils; public class ReverseTunnelTcpipChannel extends AbstractClientChannel { @@ -67,7 +66,7 @@ protected synchronized void doOpen() throws IOException { @Override protected Closeable getInnerCloseable() { - return CloseableUtils.sequential(serverSession, super.getInnerCloseable()); + return builder().sequential(serverSession, super.getInnerCloseable()).build(); } protected synchronized void doWriteData(byte[] data, int off, int len) throws IOException { diff --git a/src/main/java/org/fogbowcloud/ssh/TunnelHttpServer.java b/src/main/java/org/fogbowcloud/ssh/TunnelHttpServer.java index 5bcc210..aebad94 100644 --- a/src/main/java/org/fogbowcloud/ssh/TunnelHttpServer.java +++ b/src/main/java/org/fogbowcloud/ssh/TunnelHttpServer.java @@ -5,8 +5,10 @@ import java.io.ObjectInputStream; import java.io.UnsupportedEncodingException; import java.security.KeyPair; +import java.util.Map; -import org.apache.mina.util.Base64; +import org.apache.sshd.common.util.Base64; +import org.json.JSONObject; import fi.iki.elonen.NanoHTTPD; import fi.iki.elonen.NanoHTTPD.Response.Status; @@ -18,12 +20,12 @@ public class TunnelHttpServer extends NanoHTTPD { private KeyPair kp; public TunnelHttpServer(int httpPort, String sshTunnelHost, int sshTunnelPort, - int lowerPort, int higherPort, String hostKeyPath) { + int lowerPort, int higherPort, Long idleTokenTimeout, String hostKeyPath) { super(httpPort); this.hostKeyPath = hostKeyPath; try { this.tunneling = new TunnelServer(sshTunnelHost, sshTunnelPort, - lowerPort, higherPort, hostKeyPath); + lowerPort, higherPort, idleTokenTimeout, hostKeyPath); this.tunneling.start(); } catch (IOException e) { e.printStackTrace(); @@ -40,26 +42,30 @@ public Response serve(IHTTPSession session) { } if (splitUri[1].equals("token")) { - if (splitUri.length != 3) { + if (splitUri.length > 4) { return new NanoHTTPD.Response(Status.METHOD_NOT_ALLOWED, MIME_PLAINTEXT, ""); } String tokenId = splitUri[2]; - + if (method.equals(Method.GET)) { - Integer port = this.tunneling.getPort(tokenId); - if (port == null) { - return new NanoHTTPD.Response(Status.NOT_FOUND, - MIME_PLAINTEXT, "404 Port Not Found"); + if (splitUri.length == 4 && splitUri[3].equals("all")) { + Map ports = this.tunneling.getPortByPrefix(tokenId); + return new NanoHTTPD.Response(new JSONObject(ports).toString()); + } else { + Integer port = this.tunneling.getPort(tokenId); + if (port == null) { + return new NanoHTTPD.Response(Status.NOT_FOUND, + MIME_PLAINTEXT, "404 Port Not Found"); + } + return new NanoHTTPD.Response(port.toString()); } - return new NanoHTTPD.Response(port.toString()); } if (method.equals(Method.POST)) { Integer port = this.tunneling.createPort(tokenId); if (port == null) { - return new NanoHTTPD.Response(Status.INTERNAL_ERROR, - MIME_PLAINTEXT, "Internal error"); + return new NanoHTTPD.Response(Status.INTERNAL_ERROR, MIME_PLAINTEXT, ""); } return new NanoHTTPD.Response(port.toString()); } diff --git a/src/main/java/org/fogbowcloud/ssh/TunnelServer.java b/src/main/java/org/fogbowcloud/ssh/TunnelServer.java index 5f44e06..d609575 100644 --- a/src/main/java/org/fogbowcloud/ssh/TunnelServer.java +++ b/src/main/java/org/fogbowcloud/ssh/TunnelServer.java @@ -1,6 +1,7 @@ package org.fogbowcloud.ssh; import java.io.IOException; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -16,6 +17,7 @@ import org.apache.sshd.SshServer; import org.apache.sshd.common.ForwardingFilter; import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.Service; import org.apache.sshd.common.Session; import org.apache.sshd.common.Session.AttributeKey; import org.apache.sshd.common.SshdSocketAddress; @@ -55,13 +57,16 @@ public Token(Integer port) { private int lowerPort; private int higherPort; private String hostKeyPath; + private Long idleTokenTimeout; public TunnelServer(String sshTunnelHost, int sshTunnelPort, int lowerPort, - int higherPort, String hostKeyPath) { + int higherPort, Long idleTokenTimeout, String hostKeyPath) { this.sshTunnelHost = sshTunnelHost; this.sshTunnelPort = sshTunnelPort; this.lowerPort = lowerPort; this.higherPort = higherPort; + this.idleTokenTimeout = idleTokenTimeout == null ? TOKEN_EXPIRATION_TIMEOUT + : idleTokenTimeout; this.hostKeyPath = hostKeyPath; } @@ -77,6 +82,11 @@ public synchronized Integer createPort(String token) { newPort = port; break; } + if (newPort == null) { + LOGGER.debug("Token [" + token + "] didn't get any port. All ports are busy."); + return null; + } + LOGGER.debug("Token [" + token + "] got port [" + newPort + "]."); tokens.put(token, new Token(newPort)); return newPort; @@ -94,7 +104,14 @@ private boolean isTaken(int port) { private ReverseTunnelForwarder getActiveSession(int port) { List activeSessions = sshServer.getActiveSessions(); for (AbstractSession session : activeSessions) { - ServerConnectionService service = session.getService(ServerConnectionService.class); + Service rawService = ((ReverseTunnelSession)session).getService(); + if (rawService == null) { + continue; + } + if (!(rawService instanceof ServerConnectionService)) { + continue; + } + ServerConnectionService service = (ServerConnectionService) rawService; ReverseTunnelForwarder f = (ReverseTunnelForwarder) service.getTcpipForwarder(); for (SshdSocketAddress address : f.getLocalForwards()) { if (address.getPort() == port) { @@ -152,7 +169,7 @@ public void run() { if (token.lastIdleCheck == 0) { token.lastIdleCheck = now; } - if (now - token.lastIdleCheck > TOKEN_EXPIRATION_TIMEOUT) { + if (now - token.lastIdleCheck > idleTokenTimeout) { tokensToExpire.add(tokenEntry.getKey()); } } else { @@ -223,4 +240,31 @@ public Integer getPort(String tokenId) { return token.port; } + public Map getAllPorts() { + Map portsByPrefix = new HashMap(); + for (Entry tokenEntry : tokens.entrySet()) { + portsByPrefix.put( + tokenEntry.getKey(), + tokenEntry.getValue().port); + } + return portsByPrefix; + } + + public Map getPortByPrefix(String tokenId) { + Map portsByPrefix = new HashMap(); + Integer sshPort = getPort(tokenId); + if (sshPort != null) { + portsByPrefix.put("ssh", sshPort); + } + for (Entry tokenEntry : tokens.entrySet()) { + String tokenPrefix = tokenId + "-"; + if (tokenEntry.getKey().startsWith(tokenPrefix)) { + portsByPrefix.put( + tokenEntry.getKey().substring(tokenPrefix.length()), + tokenEntry.getValue().port); + } + } + return portsByPrefix; + } + }