blob: 75009a766c1d127e22ca450b3dbab93e07c5b574 [file] [log] [blame]
/*
* 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.common.forward;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import org.apache.sshd.client.channel.AbstractClientChannel;
import org.apache.sshd.client.future.DefaultOpenFuture;
import org.apache.sshd.client.future.OpenFuture;
import org.apache.sshd.common.Closeable;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.ChannelAsyncInputStream;
import org.apache.sshd.common.channel.ChannelAsyncOutputStream;
import org.apache.sshd.common.channel.ChannelOutputStream;
import org.apache.sshd.common.channel.LocalWindow;
import org.apache.sshd.common.future.CloseFuture;
import org.apache.sshd.common.future.DefaultCloseFuture;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.net.SshdSocketAddress;
/**
* TODO Add javadoc
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class TcpipClientChannel extends AbstractClientChannel implements ForwardingTunnelEndpointsProvider {
/**
* Type of channel being created. The type's {@link #getName()} method returns the SSH request type
*/
public enum Type implements NamedResource {
Direct("direct-tcpip"),
Forwarded("forwarded-tcpip");
public static final Set<Type> VALUES = Collections.unmodifiableSet(EnumSet.allOf(Type.class));
private final String channelType;
Type(String channelType) {
this.channelType = channelType;
}
@Override
public String getName() {
return channelType;
}
}
protected final SshdSocketAddress remote;
protected final ChannelToPortHandler port;
protected SshdSocketAddress localEntry;
private final Type typeEnum;
private SshdSocketAddress tunnelEntrance;
private SshdSocketAddress tunnelExit;
public TcpipClientChannel(Type type, IoSession serverSession, SshdSocketAddress remote) {
super(Objects.requireNonNull(type, "No type specified").getName());
this.typeEnum = type;
this.port = createChannelToPortHandler(Objects.requireNonNull(serverSession, "No server session provided"));
this.localEntry = new SshdSocketAddress((InetSocketAddress) serverSession.getLocalAddress());
this.remote = remote;
}
protected ChannelToPortHandler createChannelToPortHandler(IoSession session) {
return new ChannelToPortHandler(session, this);
}
public Type getTcpipChannelType() {
return typeEnum;
}
public void updateLocalForwardingEntry(LocalForwardingEntry entry) {
Objects.requireNonNull(entry, "No local forwarding entry provided");
// OpenSSH requires the host string it passed in the tcpip-forward global request: it compares both host and
// port and refuses the forwarding request when it doesn't match in the forwarded-tcpip request.
//
// Note: Apache MINA sshd is currently more lenient in that respect and compares only the port. RFC 4254
// states that "Implementations MUST reject these messages unless they have previously requested a remote TCP/IP
// port forwarding with the given port number"; it does not require the host name to be checked.
localEntry = new SshdSocketAddress(entry.getLocalAddress().getHostName(), entry.getBoundAddress().getPort());
}
@Override
public synchronized OpenFuture open() throws IOException {
InetSocketAddress src;
SshdSocketAddress dst;
IoSession portSession = port.getPortSession();
InetSocketAddress loc = (InetSocketAddress) portSession.getLocalAddress();
Type openType = getTcpipChannelType();
switch (openType) {
case Direct:
src = (InetSocketAddress) portSession.getRemoteAddress();
dst = this.remote;
tunnelEntrance = new SshdSocketAddress(loc.getHostString(), loc.getPort());
tunnelExit = new SshdSocketAddress(dst.getHostName(), dst.getPort());
break;
case Forwarded:
src = (InetSocketAddress) portSession.getRemoteAddress();
dst = localEntry;
tunnelEntrance = new SshdSocketAddress(src.getHostString(), src.getPort());
tunnelExit = new SshdSocketAddress(loc.getHostString(), loc.getPort());
break;
default:
throw new SshException("Unknown client channel type: " + openType);
}
if (closeFuture.isClosed()) {
throw new SshException("Session has been closed");
}
openFuture = new DefaultOpenFuture(src, futureLock);
if (log.isDebugEnabled()) {
log.debug("open({}) send SSH_MSG_CHANNEL_OPEN", this);
}
Session session = getSession();
String srcHost = src.getHostString();
String dstHost = dst.getHostName();
LocalWindow wLocal = getLocalWindow();
String type = getChannelType();
Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_OPEN,
type.length() + srcHost.length() + dstHost.length() + Long.SIZE);
buffer.putString(type);
buffer.putUInt(getChannelId());
buffer.putUInt(wLocal.getSize());
buffer.putUInt(wLocal.getPacketSize());
buffer.putString(dstHost);
buffer.putUInt(dst.getPort());
buffer.putString(srcHost);
buffer.putUInt(src.getPort());
writePacket(buffer);
return openFuture;
}
@Override
protected synchronized void doOpen() throws IOException {
if (streaming == Streaming.Async) {
asyncIn = new ChannelAsyncOutputStream(this, SshConstants.SSH_MSG_CHANNEL_DATA) {
@SuppressWarnings("synthetic-access")
@Override
protected CloseFuture doCloseGracefully() {
DefaultCloseFuture result = new DefaultCloseFuture(getChannelId(), futureLock);
CloseFuture packetsWritten = super.doCloseGracefully();
packetsWritten.addListener(p -> {
try {
// The channel writes EOF directly through the SSH session
IoWriteFuture eofSent = sendEof();
if (eofSent != null) {
eofSent.addListener(f -> result.setClosed());
return;
}
} catch (Exception e) {
getSession().exceptionCaught(e);
}
result.setClosed();
});
return result;
}
};
asyncOut = new ChannelAsyncInputStream(this);
} else {
out = new ChannelOutputStream(
this, getRemoteWindow(), log, SshConstants.SSH_MSG_CHANNEL_DATA, true);
invertedIn = out;
}
}
@Override
protected Closeable getInnerCloseable() {
return builder()
.sequential(port.getPortSession(), super.getInnerCloseable())
.build();
}
@Override
protected void doWriteData(byte[] data, int off, long len) throws IOException {
port.sendToPort(SshConstants.SSH_MSG_CHANNEL_DATA, data, off, len);
}
@Override
protected void doWriteExtendedData(byte[] data, int off, long len) throws IOException {
throw new UnsupportedOperationException(getChannelType() + " Tcpip channel does not support extended data");
}
@Override
public void handleEof() throws IOException {
super.handleEof();
port.handleEof();
}
@Override
public SshdSocketAddress getTunnelEntrance() {
return tunnelEntrance;
}
@Override
public SshdSocketAddress getTunnelExit() {
return tunnelExit;
}
}