/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sshd.server.forward; import java.io.IOException; import java.io.OutputStream; import java.net.ConnectException; import java.util.Collection; import java.util.Objects; import java.util.concurrent.ExecutorService; import org.apache.sshd.client.future.DefaultOpenFuture; import org.apache.sshd.client.future.OpenFuture; import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.RuntimeSshException; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.channel.Channel; import org.apache.sshd.common.channel.ChannelFactory; import org.apache.sshd.common.channel.ChannelOutputStream; import org.apache.sshd.common.channel.OpenChannelException; import org.apache.sshd.common.channel.Window; import org.apache.sshd.common.future.CloseFuture; import org.apache.sshd.common.io.IoConnectFuture; import org.apache.sshd.common.io.IoConnector; import org.apache.sshd.common.io.IoHandler; import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.Readable; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.common.util.net.SshdSocketAddress; import org.apache.sshd.common.util.threads.ExecutorServiceCarrier; import org.apache.sshd.common.util.threads.ThreadUtils; import org.apache.sshd.server.channel.AbstractServerChannel; /** * TODO Add javadoc * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public class TcpipServerChannel extends AbstractServerChannel { public abstract static class TcpipFactory implements ChannelFactory, ExecutorServiceCarrier { private final ForwardingFilter.Type type; protected TcpipFactory(ForwardingFilter.Type type) { this.type = type; } public final ForwardingFilter.Type getType() { return type; } @Override public final String getName() { return type.getName(); } @Override // user can override to provide an alternative public ExecutorService getExecutorService() { return null; } @Override public boolean isShutdownOnExit() { return false; } @Override public Channel create() { TcpipServerChannel channel = new TcpipServerChannel(getType()); channel.setExecutorService(getExecutorService()); channel.setShutdownOnExit(isShutdownOnExit()); return channel; } } private final ForwardingFilter.Type type; private IoConnector connector; private IoSession ioSession; private OutputStream out; public TcpipServerChannel(ForwardingFilter.Type type) { this.type = type; } public final ForwardingFilter.Type getChannelType() { return type; } @Override protected OpenFuture doInit(Buffer buffer) { String hostToConnect = buffer.getString(); int portToConnect = buffer.getInt(); String originatorIpAddress = buffer.getString(); int originatorPort = buffer.getInt(); if (log.isDebugEnabled()) { log.debug("doInit({}) Receiving request for direct tcpip: hostToConnect={}, portToConnect={}, originatorIpAddress={}, originatorPort={}", this, hostToConnect, portToConnect, originatorIpAddress, originatorPort); } final SshdSocketAddress address; switch (type) { case Direct: address = new SshdSocketAddress(hostToConnect, portToConnect); break; case Forwarded: address = service.getTcpipForwarder().getForwardedPort(portToConnect); break; default: throw new IllegalStateException("Unknown server channel type: " + type); } Session session = getSession(); FactoryManager manager = Objects.requireNonNull(session.getFactoryManager(), "No factory manager"); ForwardingFilter filter = manager.getTcpipForwardingFilter(); OpenFuture f = new DefaultOpenFuture(this); try { if ((address == null) || (filter == null) || (!filter.canConnect(type, address, session))) { if (log.isDebugEnabled()) { log.debug("doInit(" + this + ")[" + type + "][haveFilter=" + (filter != null) + "] filtered out " + address); } super.close(true); f.setException(new OpenChannelException(SshConstants.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "Connection denied")); return f; } } catch (Error e) { log.warn("doInit({})[{}] failed ({}) to consult forwarding filter: {}", session, type, e.getClass().getSimpleName(), e.getMessage()); if (log.isDebugEnabled()) { log.debug("doInit(" + this + ")[" + type + "] filter consultation failure details", e); } throw new RuntimeSshException(e); } // TODO: revisit for better threading. Use async io ? out = new ChannelOutputStream(this, getRemoteWindow(), log, SshConstants.SSH_MSG_CHANNEL_DATA, true); IoHandler handler = new IoHandler() { @SuppressWarnings("synthetic-access") @Override public void messageReceived(IoSession session, Readable message) throws Exception { if (isClosing()) { if (log.isDebugEnabled()) { log.debug("doInit({}) Ignoring write to channel in CLOSING state", TcpipServerChannel.this); } } else { Buffer buffer = new ByteArrayBuffer(message.available() + Long.SIZE, false); buffer.putBuffer(message); out.write(buffer.array(), buffer.rpos(), buffer.available()); out.flush(); } } @Override public void sessionCreated(IoSession session) throws Exception { // ignored } @Override public void sessionClosed(IoSession session) throws Exception { close(false); } @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { close(true); } }; connector = manager.getIoServiceFactory().createConnector(handler); IoConnectFuture future = connector.connect(address.toInetSocketAddress()); future.addListener(future1 -> handleChannelConnectResult(f, future1)); return f; } protected void handleChannelConnectResult(OpenFuture f, IoConnectFuture future) { try { if (future.isConnected()) { handleChannelOpenSuccess(f, future.getSession()); return; } Throwable problem = GenericUtils.peelException(future.getException()); if (problem != null) { handleChannelOpenFailure(f, problem); } } catch (RuntimeException t) { Throwable e = GenericUtils.peelException(t); signalChannelOpenFailure(e); try { f.setException(e); } finally { notifyStateChanged(e.getClass().getSimpleName()); } } } protected void handleChannelOpenSuccess(OpenFuture f, IoSession session) { ioSession = session; String changeEvent = session.toString(); try { signalChannelOpenSuccess(); f.setOpened(); } catch (Throwable t) { Throwable e = GenericUtils.peelException(t); changeEvent = e.getClass().getSimpleName(); signalChannelOpenFailure(e); f.setException(e); } finally { notifyStateChanged(changeEvent); } } protected void handleChannelOpenFailure(OpenFuture f, Throwable problem) { signalChannelOpenFailure(problem); notifyStateChanged(problem.getClass().getSimpleName()); closeImmediately0(); if (problem instanceof ConnectException) { f.setException(new OpenChannelException(SshConstants.SSH_OPEN_CONNECT_FAILED, problem.getMessage(), problem)); } else { f.setException(problem); } } private void closeImmediately0() { // We need to close the channel immediately to remove it from the // server session's channel table and *not* send a packet to the // client. A notification was already sent by our caller, or will // be sent after we return. // super.close(true); // We also need to dispose of the connector, but unfortunately we // are being invoked by the connector thread or the connector's // own processor thread. Disposing of the connector within either // causes deadlock. Instead create a thread to dispose of the // connector in the background. ExecutorService service = getExecutorService(); // allocate a temporary executor service if none provided final ExecutorService executors = (service == null) ? ThreadUtils.newSingleThreadExecutor("TcpIpServerChannel-ConnectorCleanup[" + getSession() + "]") : service; // shutdown the temporary executor service if had to create it final boolean shutdown = executors != service || isShutdownOnExit(); executors.submit(() -> { try { connector.close(true); } finally { if (shutdown && !executors.isShutdown()) { Collection<Runnable> runners = executors.shutdownNow(); if (log.isDebugEnabled()) { log.debug("destroy({}) - shutdown executor service - runners count={}", TcpipServerChannel.this, runners.size()); } } } }); } @Override public CloseFuture close(boolean immediately) { return super.close(immediately).addListener(sshFuture -> closeImmediately0()); } @Override protected void doWriteData(byte[] data, int off, long len) throws IOException { ValidateUtils.checkTrue(len <= Integer.MAX_VALUE, "Data length exceeds int boundaries: %d", len); // Make sure we copy the data as the incoming buffer may be reused Buffer buf = ByteArrayBuffer.getCompactClone(data, off, (int) len); ioSession.write(buf).addListener(future -> { if (future.isWritten()) { handleWriteDataSuccess(SshConstants.SSH_MSG_CHANNEL_DATA, buf.array(), 0, (int) len); } else { handleWriteDataFailure(SshConstants.SSH_MSG_CHANNEL_DATA, buf.array(), 0, (int) len, future.getException()); } }); } @Override protected void doWriteExtendedData(byte[] data, int off, long len) throws IOException { throw new UnsupportedOperationException(type + "Tcpip channel does not support extended data"); } protected void handleWriteDataSuccess(byte cmd, byte[] data, int off, int len) { Session session = getSession(); try { Window wLocal = getLocalWindow(); wLocal.consumeAndCheck(len); } catch (Throwable e) { if (log.isDebugEnabled()) { log.debug("handleWriteDataSuccess({})[{}] failed ({}) to consume len={}: {}", this, SshConstants.getCommandMessageName(cmd & 0xFF), e.getClass().getSimpleName(), len, e.getMessage()); } session.exceptionCaught(e); } } protected void handleWriteDataFailure(byte cmd, byte[] data, int off, int len, Throwable t) { Session session = getSession(); if (log.isDebugEnabled()) { log.debug("handleWriteDataFailure({})[{}] failed ({}) to write len={}: {}", this, SshConstants.getCommandMessageName(cmd & 0xFF), t.getClass().getSimpleName(), len, t.getMessage()); } if (log.isTraceEnabled()) { log.trace("doWriteData(" + this + ")[" + SshConstants.getCommandMessageName(cmd & 0xFF) + "]" + " len=" + len + " write failure details", t); } session.exceptionCaught(t); } }