package com.sun.webkit.network;
import com.sun.javafx.logging.PlatformLogger;
import com.sun.javafx.logging.PlatformLogger.Level;
import com.sun.webkit.Invoker;
import com.sun.webkit.WebPage;
import java.io.IOException;
import java.io.InputStream;
import static java.lang.String.format;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.NoRouteToHostException;
import java.net.PortUnreachableException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.List;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
final class SocketStreamHandle {
private static final Pattern FIRST_LINE_PATTERN = Pattern.compile(
"^HTTP/1.[01]\\s+(\\d{3})(?:\\s.*)?$");
private static final PlatformLogger logger = PlatformLogger.getLogger(
SocketStreamHandle.class.getName());
private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
10, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new CustomThreadFactory());
private enum State {ACTIVE, CLOSE_REQUESTED, DISPOSED}
private final String host;
private final int port;
private final boolean ssl;
private final WebPage webPage;
private final long data;
private volatile Socket socket;
private volatile State state = State.ACTIVE;
private volatile boolean connected;
private SocketStreamHandle(String host, int port, boolean ssl,
WebPage webPage, long data)
{
this.host = host;
this.port = port;
this.ssl = ssl;
this.webPage = webPage;
this.data = data;
}
private static SocketStreamHandle fwkCreate(String host, int port,
boolean ssl, WebPage webPage,
long data)
{
final SocketStreamHandle ssh =
new SocketStreamHandle(host, port, ssl, webPage, data);
logger.finest("Starting {0}", ssh);
threadPool.submit(() -> {
ssh.run();
});
return ssh;
}
private void run() {
if (webPage == null) {
logger.finest("{0} is not associated with any web "
+ "page, aborted", this);
didFail(0, "Web socket is not associated with any web page");
didClose();
return;
}
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
doRun();
return null;
}, webPage.getAccessControlContext());
}
private void doRun() {
Throwable error = null;
String errorDescription = null;
try {
logger.finest("{0} started", this);
connect();
connected = true;
logger.finest("{0} connected", this);
didOpen();
InputStream is = socket.getInputStream();
while (true) {
byte[] buffer = new byte[8192];
int n = is.read(buffer);
if(n > 0) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s received len: [%d], data:%s",
this, n, dump(buffer, n)));
}
didReceiveData(buffer, n);
} else {
logger.finest("{0} connection closed by remote host", this);
break;
}
}
} catch (UnknownHostException ex) {
error = ex;
errorDescription = "Unknown host";
} catch (ConnectException ex) {
error = ex;
errorDescription = "Unable to connect";
} catch (NoRouteToHostException ex) {
error = ex;
errorDescription = "No route to host";
} catch (PortUnreachableException ex) {
error = ex;
errorDescription = "Port unreachable";
} catch (SocketException ex) {
if (state != State.ACTIVE) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s exception (most "
+ "likely caused by local close)", this), ex);
}
} else {
error = ex;
errorDescription = "Socket error";
}
} catch (SSLException ex) {
error = ex;
errorDescription = "SSL error";
} catch (IOException ex) {
error = ex;
errorDescription = "I/O error";
} catch (SecurityException ex) {
error = ex;
errorDescription = "Security error";
} catch (Throwable th) {
error = th;
}
if (error != null) {
if (errorDescription == null) {
errorDescription = "Unknown error";
logger.warning(format("%s unexpected error", this), error);
} else {
logger.finest(format("%s exception", this), error);
}
didFail(0, errorDescription);
}
try {
socket.close();
} catch (IOException ignore) {}
didClose();
logger.finest("{0} finished", this);
}
private void connect() throws IOException {
SecurityManager securityManager = System.getSecurityManager();
if (securityManager != null) {
securityManager.checkConnect(host, port);
}
boolean success = false;
IOException lastException = null;
boolean triedDirectConnection = false;
ProxySelector proxySelector = AccessController.doPrivileged(
(PrivilegedAction<ProxySelector>) () -> ProxySelector.getDefault());
if (proxySelector != null) {
URI uri;
try {
uri = new URI((ssl ? "https" : "http") + "://" + host);
} catch (URISyntaxException ex) {
throw new IOException(ex);
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s selecting proxies for: [%s]", this, uri));
}
List<Proxy> proxies = proxySelector.select(uri);
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s selected proxies: %s", this, proxies));
}
for (Proxy proxy : proxies) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s trying proxy: [%s]", this, proxy));
}
if (proxy.type() == Proxy.Type.DIRECT) {
triedDirectConnection = true;
}
try {
connect(proxy);
success = true;
break;
} catch (IOException ex) {
logger.finest(format("%s exception", this), ex);
lastException = ex;
if (proxy.address() != null) {
proxySelector.connectFailed(uri, proxy.address(), ex);
}
continue;
}
}
}
if (!success && !triedDirectConnection) {
logger.finest("{0} trying direct connection", this);
connect(Proxy.NO_PROXY);
success = true;
}
if (!success) {
throw lastException;
}
}
private void connect(Proxy proxy) throws IOException {
synchronized (this) {
if (state != State.ACTIVE) {
throw new SocketException("Close requested");
}
socket = new Socket(proxy);
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s connecting to: [%s:%d]",
this, host, port));
}
socket.connect(new InetSocketAddress(host, port));
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s connected to: [%s:%d]",
this, host, port));
}
if (ssl) {
synchronized (this) {
if (state != State.ACTIVE) {
throw new SocketException("Close requested");
}
logger.finest("{0} starting SSL handshake", this);
socket = HttpsURLConnection.getDefaultSSLSocketFactory()
.createSocket(socket, host, port, true);
}
((SSLSocket) socket).startHandshake();
}
}
private int fwkSend(byte[] buffer) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s sending len: [%d], data:%s",
this, buffer.length, dump(buffer, buffer.length)));
}
if (connected) {
try {
socket.getOutputStream().write(buffer);
return buffer.length;
} catch (IOException ex) {
logger.finest(format("%s exception", this), ex);
didFail(0, "I/O error");
return 0;
}
} else {
logger.finest("{0} not connected", this);
didFail(0, "Not connected");
return 0;
}
}
private void fwkClose() {
synchronized (this) {
logger.finest("{0}", this);
state = State.CLOSE_REQUESTED;
try {
if (socket != null) {
socket.close();
}
} catch (IOException ignore) {}
}
}
private void fwkNotifyDisposed() {
logger.finest("{0}", this);
state = State.DISPOSED;
}
private void didOpen() {
Invoker.getInvoker().postOnEventThread(() -> {
if (state == State.ACTIVE) {
notifyDidOpen();
}
});
}
private void didReceiveData(final byte[] buffer, final int len) {
Invoker.getInvoker().postOnEventThread(() -> {
if (state == State.ACTIVE) {
notifyDidReceiveData(buffer, len);
}
});
}
private void didFail(final int errorCode, final String errorDescription) {
Invoker.getInvoker().postOnEventThread(() -> {
if (state == State.ACTIVE) {
notifyDidFail(errorCode, errorDescription);
}
});
}
private void didClose() {
Invoker.getInvoker().postOnEventThread(() -> {
if (state != State.DISPOSED) {
notifyDidClose();
}
});
}
private void notifyDidOpen() {
logger.finest("{0}", this);
twkDidOpen(data);
}
private void notifyDidReceiveData(byte[] buffer, int len) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s, len: [%d], data:%s",
this, len, dump(buffer, len)));
}
twkDidReceiveData(buffer, len, data);
}
private void notifyDidFail(int errorCode, String errorDescription) {
if (logger.isLoggable(Level.FINEST)) {
logger.finest(format("%s, errorCode: %d, "
+ "errorDescription: %s",
this, errorCode, errorDescription));
}
twkDidFail(errorCode, errorDescription, data);
}
private void notifyDidClose() {
logger.finest("{0}", this);
twkDidClose(data);
}
private static native void twkDidOpen(long data);
private static native void twkDidReceiveData(byte[] buffer, int len,
long data);
private static native void twkDidFail(int errorCode,
String errorDescription, long data);
private static native void twkDidClose(long data);
private static String dump(byte[] buffer, int len) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < len) {
StringBuilder c1 = new StringBuilder();
StringBuilder c2 = new StringBuilder();
for (int k = 0; k < 16; k++, i++) {
if (i < len) {
int b = buffer[i] & 0xff;
c1.append(format("%02x ", b));
c2.append((b >= 0x20 && b <= 0x7e) ? (char) b : '.');
} else {
c1.append(" ");
}
}
sb.append(format("%n ")).append(c1).append(' ').append(c2);
}
return sb.toString();
}
@Override
public String toString() {
return format("SocketStreamHandle{host=%s, port=%d, ssl=%s, "
+ "data=0x%016X, state=%s, connected=%s}",
host, port, ssl, data, state, connected);
}
private static final class CustomThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger index = new AtomicInteger(1);
private CustomThreadFactory() {
SecurityManager sm = System.getSecurityManager();
group = (sm != null) ? sm.getThreadGroup()
: Thread.currentThread().getThreadGroup();
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, "SocketStreamHandle-"
+ index.getAndIncrement());
t.setDaemon(true);
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
}