package org.arquillian.cube.kubernetes.impl.portforward;
import io.fabric8.kubernetes.clnt.v2_2.Config;
import io.fabric8.kubernetes.clnt.v2_2.internal.SSLUtils;
import io.undertow.UndertowOptions;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.client.UndertowClient;
import io.undertow.client.spdy.SpdyClientConnection;
import io.undertow.connector.ByteBufferPool;
import io.undertow.protocols.spdy.SpdyChannel;
import io.undertow.protocols.spdy.SpdyChannelWithoutFlowControl;
import io.undertow.server.XnioByteBufferPool;
import io.undertow.util.Headers;
import io.undertow.util.Methods;
import io.undertow.util.StringReadChannelListener;
import java.io.Closeable;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.xnio.BufferAllocator;
import org.xnio.ByteBufferSlicePool;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import org.xnio.IoFuture;
import org.xnio.IoUtils;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Pool;
import org.xnio.StreamConnection;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.xnio.channels.AcceptingChannel;
import org.xnio.ssl.XnioSsl;
public final class PortForwarder implements Closeable {
private static final String PORT_FWD = "%sapi/v1/namespaces/%s/pods/%s/portforward";
private static final AtomicInteger requestId = new AtomicInteger();
private final OptionMap DEFAULT_OPTIONS;
private InetAddress portForwardBindAddress;
private URI portForwardURI;
private Pool<ByteBuffer> bufferPoolSlice;
private ByteBufferPool bufferPool;
private XnioWorker xnioWorker;
private ClientConnection connection;
private Collection<PortForwardServer> servers = new ArrayList<>();
public PortForwarder(Config config, String podName) throws Exception {
try {
this.portForwardURI =
URI.create(String.format(PORT_FWD, config.getMasterUrl(), config.getNamespace(), podName));
final Xnio xnio = Xnio.getInstance();
DEFAULT_OPTIONS = OptionMap.builder()
.set(Options.WORKER_NAME, String.format("PortForwarding for %s/%s", config.getNamespace(), podName))
.set(Options.WORKER_IO_THREADS, 16)
.set(Options.CONNECTION_HIGH_WATER, 400)
.set(Options.CONNECTION_LOW_WATER, 200)
.set(Options.WORKER_TASK_CORE_THREADS, 16)
.set(Options.WORKER_TASK_MAX_THREADS, 128)
.set(Options.TCP_NODELAY, true)
.set(Options.KEEP_ALIVE, true)
.set(Options.SSL_PROTOCOL, "TLS")
//.set(Options.CORK, true)
.getMap();
// XXX: hard-coding trust all certs
final XnioSsl xnioSsl = xnio.getSslProvider(SSLUtils.keyManagers(config),
new TrustManager[] {new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String s) {
}
public void checkServerTrusted(X509Certificate[] chain, String s) {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}}, DEFAULT_OPTIONS);
this.xnioWorker = xnio.createWorker(null, DEFAULT_OPTIONS);
bufferPoolSlice =
new ByteBufferSlicePool(BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR, 17 * 1024, 17 * 1024 * 20);
bufferPool = new XnioByteBufferPool(bufferPoolSlice);
IoFuture<ClientConnection> connectFuture =
UndertowClient.getInstance().connect(portForwardURI, xnioWorker, xnioSsl, bufferPool, DEFAULT_OPTIONS);
// XXX: use timeout
connection = connectFuture.getInterruptibly();
// Establish the connection
ClientRequest request = new ClientRequest()
.setMethod(Methods.POST)
.setPath(portForwardURI.getPath());
request.getRequestHeaders()
.put(Headers.HOST, this.portForwardURI.getHost())
.put(Headers.CONNECTION, "Upgrade")
.put(Headers.UPGRADE, "SPDY/3.1");
if (config.getOauthToken() != null) {
request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer " + config.getOauthToken());
}
final CountDownLatch latch = new CountDownLatch(1);
final IOException[] holder = new IOException[1];
connection.sendRequest(request, new ClientCallback<ClientExchange>() {
@Override
public void completed(ClientExchange result) {
result.setResponseListener(new ClientCallback<ClientExchange>() {
@Override
public void completed(ClientExchange result) {
try {
upgradeConnection(result);
} catch (Exception e) {
holder[0] = (IOException) new IOException("Unexpected error", e).fillInStackTrace();
} finally {
latch.countDown();
}
}
@Override
public void failed(IOException e) {
holder[0] = e;
latch.countDown();
}
});
}
@Override
public void failed(IOException e) {
holder[0] = e;
latch.countDown();
}
});
latch.await();
if (holder[0] != null) {
throw new IOException("Failed to establish portforward client connection", holder[0]);
}
} catch (Throwable t) {
if (connection != null) {
IoUtils.safeClose(connection);
}
if (xnioWorker != null) {
xnioWorker.shutdown();
}
throw t;
}
}
public static void main(String[] args) throws Exception {
if (args.length < 4) {
System.out.println(
"Usage: portforward <namespace> <pod> <source-port> <target-port> -b [optional] <bindAddress>");
System.out.println("Example: portforward mynamespace somepod 8080 8080 -b 10.1.1.1");
}
final String namespace = args[0];
final String podName = args[1];
final int sourcePort = Integer.valueOf(args[2]);
final int targetPort = Integer.valueOf(args[3]);
InetAddress bindAddress = null;
for (int i = 0; i < args.length; i++) {
if (args[i].contains("-b")) {
bindAddress = InetAddress.getByName(args[i + 1]);
}
}
final Config config = new Config();
config.setNamespace(namespace);
final PortForwarder forwarder = new PortForwarder(config, podName);
forwarder.setPortForwardBindAddress(bindAddress);
final PortForwardServer server = forwarder.forwardPort(sourcePort, targetPort);
System.in.read();
server.close();
forwarder.close();
}
public synchronized PortForwardServer forwardPort(int sourcePort, int targetPort)
throws IllegalArgumentException, IOException {
PortForwardServer server = new PortForwardServer(createServer(sourcePort, targetPort), targetPort);
servers.add(server);
return server;
}
public synchronized void close() {
for (PortForwardServer server : servers) {
IoUtils.safeClose(server.server);
}
servers.clear();
IoUtils.safeClose(connection);
connection = null;
xnioWorker.shutdown();
xnioWorker = null;
}
public void setPortForwardBindAddress(InetAddress bindAddress) {
if (bindAddress == null) {
try {
bindAddress = Inet4Address.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
portForwardBindAddress = bindAddress;
}
private synchronized void close(PortForwardServer server) {
IoUtils.safeClose(server.server);
servers.remove(server);
}
private AcceptingChannel<? extends StreamConnection> createServer(int sourcePort, int targetPort)
throws IllegalArgumentException, IOException {
OptionMap socketOptions = OptionMap.builder()
.set(Options.WORKER_IO_THREADS, 16)
.set(Options.TCP_NODELAY, true)
.set(Options.REUSE_ADDRESSES, true)
.getMap();
ChannelListener<AcceptingChannel<StreamConnection>> acceptListener = ChannelListeners.openListenerAdapter(
new PortForwardOpenListener(connection, portForwardURI.getPath(), targetPort, requestId, bufferPoolSlice,
OptionMap.EMPTY));
AcceptingChannel<? extends StreamConnection> server =
xnioWorker.createStreamConnectionServer(new InetSocketAddress(portForwardBindAddress, sourcePort),
acceptListener, socketOptions);
server.resumeAccepts();
return server;
}
private void upgradeConnection(ClientExchange result) throws IOException {
if (result.getResponse().getResponseCode() == 101) {
// flush response
new StringReadChannelListener(bufferPool) {
@Override
protected void stringDone(String string) {
}
@Override
protected void error(IOException e) {
}
}.setup(result.getResponseChannel());
// Create the upgraded SPDY connection
ByteBufferPool heapBufferPool =
new XnioByteBufferPool(new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 8196, 8196));
SpdyChannel spdyChannel =
new SpdyChannelWithoutFlowControl(connection.performUpgrade(), bufferPool, null, heapBufferPool, true,
OptionMap.EMPTY);
Integer idleTimeout = DEFAULT_OPTIONS.get(UndertowOptions.IDLE_TIMEOUT);
if (idleTimeout != null && idleTimeout > 0) {
spdyChannel.setIdleTimeout(idleTimeout);
}
connection = new SpdyClientConnection(spdyChannel, null);
} else {
throw new IOException("Failed to upgrade connection");
}
}
public final class PortForwardServer {
private final AcceptingChannel<? extends StreamConnection> server;
private final int targetPort;
private PortForwardServer(AcceptingChannel<? extends StreamConnection> server, int targetPort) {
this.server = server;
this.targetPort = targetPort;
}
public int getSourcePort() {
return getLocalAddress().getPort();
}
public int getTargetPort() {
return targetPort;
}
public InetSocketAddress getLocalAddress() {
return server.getLocalAddress(InetSocketAddress.class);
}
public void close() {
PortForwarder.this.close(this);
}
}
}