/*******************************************************************************
* Copyright (c) 2015 Red Hat.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat - Initial Contribution
*******************************************************************************/
package org.eclipse.linuxtools.internal.docker.core;
import java.io.FilterInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.Socket;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.linuxtools.docker.core.Activator;
import org.osgi.framework.Bundle;
import com.spotify.docker.client.LogReader;
import com.spotify.docker.client.LogStream;
/**
* This is a workaround for lack of HTTP Hijacking support in Apache
* HTTPClient. The assumptions made in Apache HTTPClient are that a
* response is an InputStream and so we have no sane way to access the
* underlying OutputStream (which exists at the socket level)
*
* References :
* https://docs.docker.com/reference/api/docker_remote_api_v1.16/#32-hijacking
* https://github.com/docker/docker/issues/5933
*/
public class HttpHijackWorkaround {
public static WritableByteChannel getOutputStream(LogStream stream, String uri) throws Exception {
final String[] fields = new String[] {
"reader", //$NON-NLS-1$
"stream", //$NON-NLS-1$
"original", //$NON-NLS-1$
"input", //$NON-NLS-1$
"in", //$NON-NLS-1$
"in", //$NON-NLS-1$
"wrappedStream", //$NON-NLS-1$
"in", //$NON-NLS-1$
"instream" //$NON-NLS-1$
};
final String[] declared = new String[] {
"com.spotify.docker.client.DefaultLogStream",
LogReader.class.getName(),
"org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream", //$NON-NLS-1$
"org.glassfish.jersey.message.internal.EntityInputStream", //$NON-NLS-1$
FilterInputStream.class.getName(),
FilterInputStream.class.getName(),
"org.apache.http.conn.EofSensorInputStream", //$NON-NLS-1$
"org.apache.http.impl.io.IdentityInputStream", //$NON-NLS-1$
"org.apache.http.impl.io.SessionInputBufferImpl" }; //$NON-NLS-1$
final String [] bundles = new String[] {
"org.glassfish.jersey.core.jersey-common", //$NON-NLS-1$
"org.apache.httpcomponents.httpcore", //$NON-NLS-1$
"org.apache.httpcomponents.httpclient" //$NON-NLS-1$
};
List<String[]> list = new LinkedList<>();
for (int i = 0; i < fields.length; i++) {
list.add(new String[] { declared[i], fields[i] });
}
if (uri.startsWith("unix:")) { //$NON-NLS-1$
list.add(new String[] { "sun.nio.ch.ChannelInputStream", "ch" }); //$NON-NLS-1$ //$NON-NLS-2$
} else if (uri.startsWith("https:")) { //$NON-NLS-1$
float jvmVersion = Float.parseFloat(System.getProperty("java.specification.version")); //$NON-NLS-1$
String fName;
if (jvmVersion < 1.9f) {
fName = "c"; //$NON-NLS-1$
} else {
fName = "socket"; //$NON-NLS-1$
}
list.add(new String[] { "sun.security.ssl.AppInputStream", fName }); //$NON-NLS-1$
} else {
list.add(new String[] { "java.net.SocketInputStream", "socket" }); //$NON-NLS-1$ //$NON-NLS-2$
}
Object res = getInternalField(stream, list, bundles);
if (res instanceof WritableByteChannel) {
return (WritableByteChannel) res;
} else if (res instanceof Socket) {
return Channels.newChannel(((Socket) res).getOutputStream());
} else {
// TODO: throw an exception and let callers handle it.
return null;
}
}
/*
* We could add API for this in com.spotify.docker.client since there is
* access to the underlying InputStream but better wait and see what
* happens with the HTTP Hijacking situation.
*/
public static InputStream getInputStream(LogStream stream) {
final String[] fields = new String[] { "reader", "stream" }; //$NON-NLS-1$ //$NON-NLS-2$
final String[] declared = new String[] { "com.spotify.docker.client.DefaultLogStream", LogReader.class.getName()};
List<String[]> list = new LinkedList<>();
for (int i = 0; i < fields.length; i++) {
list.add(new String[] { declared[i], fields[i] });
}
return (InputStream) getInternalField(stream, list, new String [0]);
}
/*
* Access arbitrarily nested internal fields.
*/
private static Object getInternalField (Object input, List<String []> set, String [] bundles) {
Object curr = input;
try {
for (String [] e : set) {
Field f = loadClass(e[0], bundles).getDeclaredField(e[1]);
f.setAccessible(true);
curr = f.get(curr);
}
} catch (Exception e) {
Activator.log(e);
}
return curr;
}
/*
* Avoid explicitly depending on certain classes that are requirements
* of the docker-client library (com.spotify.docker.client).
*/
private static Class<?> loadClass(String key, String [] bundles) {
try {
return Class.forName(key);
} catch (ClassNotFoundException e) {
for (String bsName : bundles) {
Bundle b = Platform.getBundle(bsName);
try {
return b.loadClass(key);
} catch (ClassNotFoundException e1) {
}
}
}
return null;
}
}