1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.webkit.network;
  27 
  28 import com.sun.javafx.logging.PlatformLogger;
  29 import com.sun.javafx.logging.PlatformLogger.Level;
  30 import com.sun.webkit.Invoker;
  31 import com.sun.webkit.WebPage;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import static java.lang.String.format;
  35 import java.net.ConnectException;
  36 import java.net.InetSocketAddress;
  37 import java.net.NoRouteToHostException;
  38 import java.net.PortUnreachableException;
  39 import java.net.Proxy;
  40 import java.net.ProxySelector;
  41 import java.net.Socket;
  42 import java.net.SocketException;
  43 import java.net.URI;
  44 import java.net.URISyntaxException;
  45 import java.net.UnknownHostException;
  46 import java.security.AccessController;
  47 import java.security.PrivilegedAction;
  48 import java.util.List;
  49 import java.util.concurrent.SynchronousQueue;
  50 import java.util.concurrent.ThreadFactory;
  51 import java.util.concurrent.ThreadPoolExecutor;
  52 import java.util.concurrent.TimeUnit;
  53 import java.util.concurrent.atomic.AtomicInteger;
  54 import java.util.regex.Pattern;
  55 import javax.net.ssl.HttpsURLConnection;
  56 import javax.net.ssl.SSLException;
  57 import javax.net.ssl.SSLSocket;
  58 
  59 final class SocketStreamHandle {
  60     private static final Pattern FIRST_LINE_PATTERN = Pattern.compile(
  61             "^HTTP/1.[01]\\s+(\\d{3})(?:\\s.*)?$");
  62     private static final PlatformLogger logger = PlatformLogger.getLogger(
  63             SocketStreamHandle.class.getName());
  64     private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
  65             0, Integer.MAX_VALUE,
  66             10, TimeUnit.SECONDS,
  67             new SynchronousQueue<Runnable>(),
  68             new CustomThreadFactory());
  69 
  70     private enum State {ACTIVE, CLOSE_REQUESTED, DISPOSED}
  71 
  72     private final String host;
  73     private final int port;
  74     private final boolean ssl;
  75     private final WebPage webPage;
  76     private final long data;
  77     private volatile Socket socket;
  78     private volatile State state = State.ACTIVE;
  79     private volatile boolean connected;
  80 
  81     private SocketStreamHandle(String host, int port, boolean ssl,
  82                                WebPage webPage, long data)
  83     {
  84         this.host = host;
  85         this.port = port;
  86         this.ssl = ssl;
  87         this.webPage = webPage;
  88         this.data = data;
  89     }
  90 
  91     private static SocketStreamHandle fwkCreate(String host, int port,
  92                                                 boolean ssl, WebPage webPage,
  93                                                 long data)
  94     {
  95         final SocketStreamHandle ssh =
  96                 new SocketStreamHandle(host, port, ssl, webPage, data);
  97         logger.finest("Starting {0}", ssh);
  98         threadPool.submit(() -> {
  99             ssh.run();
 100         });
 101         return ssh;
 102     }
 103 
 104     private void run() {
 105         if (webPage == null) {
 106             logger.finest("{0} is not associated with any web "
 107                     + "page, aborted", this);
 108             // In theory we could pump this error through the doRun()'s
 109             // error handling code but in that case that error handling
 110             // code would have to run outside the doPrivileged block,
 111             // which is something we want to avoid.
 112             didFail(0, "Web socket is not associated with any web page");
 113             didClose();
 114             return;
 115         }
 116         AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 117             doRun();
 118             return null;
 119         }, webPage.getAccessControlContext());
 120     }
 121 
 122     private void doRun() {
 123         Throwable error = null;
 124         String errorDescription = null;
 125         try {
 126             logger.finest("{0} started", this);
 127             connect();
 128             connected = true;
 129             logger.finest("{0} connected", this);
 130             didOpen();
 131             InputStream is = socket.getInputStream();
 132             while (true) {
 133                 byte[] buffer = new byte[8192];
 134                 int n = is.read(buffer);
 135                 if(n > 0) {
 136                     if (logger.isLoggable(Level.FINEST)) {
 137                         logger.finest(format("%s received len: [%d], data:%s",
 138                                 this, n, dump(buffer, n)));
 139                     }
 140                     didReceiveData(buffer, n);
 141                 } else {
 142                     logger.finest("{0} connection closed by remote host", this);
 143                     break;
 144                 }
 145             }
 146         } catch (UnknownHostException ex) {
 147             error = ex;
 148             errorDescription = "Unknown host";
 149         } catch (ConnectException ex) {
 150             error = ex;
 151             errorDescription = "Unable to connect";
 152         } catch (NoRouteToHostException ex) {
 153             error = ex;
 154             errorDescription = "No route to host";
 155         } catch (PortUnreachableException ex) {
 156             error = ex;
 157             errorDescription = "Port unreachable";
 158         } catch (SocketException ex) {
 159             if (state != State.ACTIVE) {
 160                 if (logger.isLoggable(Level.FINEST)) {
 161                     logger.finest(format("%s exception (most "
 162                             + "likely caused by local close)", this), ex);
 163                 }
 164             } else {
 165                 error = ex;
 166                 errorDescription = "Socket error";
 167             }
 168         } catch (SSLException ex) {
 169             error = ex;
 170             errorDescription = "SSL error";
 171         } catch (IOException ex) {
 172             error = ex;
 173             errorDescription = "I/O error";
 174         } catch (SecurityException ex) {
 175             error = ex;
 176             errorDescription = "Security error";
 177         } catch (Throwable th) {
 178             error = th;
 179         }
 180 
 181         if (error != null) {
 182             if (errorDescription == null) {
 183                 errorDescription = "Unknown error";
 184                 logger.warning(format("%s unexpected error", this), error);
 185             } else {
 186                 logger.finest(format("%s exception", this), error);
 187             }
 188             didFail(0, errorDescription);
 189         }
 190 
 191         try {
 192             socket.close();
 193         } catch (IOException ignore) {}
 194         didClose();
 195 
 196         logger.finest("{0} finished", this);
 197     }
 198 
 199     private void connect() throws IOException {
 200         SecurityManager securityManager = System.getSecurityManager();
 201         if (securityManager != null) {
 202             securityManager.checkConnect(host, port);
 203         }
 204 
 205         // The proxy trial logic here is meant to mimic
 206         // sun.net.www.protocol.http.HttpURLConnection.plainConnect
 207         boolean success = false;
 208         IOException lastException = null;
 209         boolean triedDirectConnection = false;
 210         ProxySelector proxySelector = AccessController.doPrivileged(
 211                 (PrivilegedAction<ProxySelector>) () -> ProxySelector.getDefault());
 212         if (proxySelector != null) {
 213             URI uri;
 214             try {
 215                 uri = new URI((ssl ? "https" : "http") + "://" + host);
 216             } catch (URISyntaxException ex) {
 217                 throw new IOException(ex);
 218             }
 219             if (logger.isLoggable(Level.FINEST)) {
 220                 logger.finest(format("%s selecting proxies for: [%s]", this, uri));
 221             }
 222             List<Proxy> proxies = proxySelector.select(uri);
 223             if (logger.isLoggable(Level.FINEST)) {
 224                 logger.finest(format("%s selected proxies: %s", this, proxies));
 225             }
 226             for (Proxy proxy : proxies) {
 227                 if (logger.isLoggable(Level.FINEST)) {
 228                     logger.finest(format("%s trying proxy: [%s]", this, proxy));
 229                 }
 230                 if (proxy.type() == Proxy.Type.DIRECT) {
 231                     triedDirectConnection = true;
 232                 }
 233                 try {
 234                     connect(proxy);
 235                     success = true;
 236                     break;
 237                 } catch (IOException ex) {
 238                     logger.finest(format("%s exception", this), ex);
 239                     lastException = ex;
 240                     if (proxy.address() != null) {
 241                         proxySelector.connectFailed(uri, proxy.address(), ex);
 242                     }
 243                     continue;
 244                 }
 245             }
 246         }
 247         if (!success && !triedDirectConnection) {
 248             logger.finest("{0} trying direct connection", this);
 249             connect(Proxy.NO_PROXY);
 250             success = true;
 251         }
 252         if (!success) {
 253             throw lastException;
 254         }
 255     }
 256 
 257     private void connect(Proxy proxy) throws IOException {
 258         synchronized (this) {
 259             if (state != State.ACTIVE) {
 260                 throw new SocketException("Close requested");
 261             }
 262             socket = new Socket(proxy);
 263         }
 264         if (logger.isLoggable(Level.FINEST)) {
 265             logger.finest(format("%s connecting to: [%s:%d]",
 266                     this, host, port));
 267         }
 268         socket.connect(new InetSocketAddress(host, port));
 269         if (logger.isLoggable(Level.FINEST)) {
 270             logger.finest(format("%s connected to: [%s:%d]",
 271                     this, host, port));
 272         }
 273         if (ssl) {
 274             synchronized (this) {
 275                 if (state != State.ACTIVE) {
 276                     throw new SocketException("Close requested");
 277                 }
 278                 logger.finest("{0} starting SSL handshake", this);
 279                 socket = HttpsURLConnection.getDefaultSSLSocketFactory()
 280                         .createSocket(socket, host, port, true);
 281             }
 282             ((SSLSocket) socket).startHandshake();
 283         }
 284     }
 285 
 286     private int fwkSend(byte[] buffer) {
 287         if (logger.isLoggable(Level.FINEST)) {
 288             logger.finest(format("%s sending len: [%d], data:%s",
 289                     this, buffer.length, dump(buffer, buffer.length)));
 290         }
 291         if (connected) {
 292             try {
 293                 socket.getOutputStream().write(buffer);
 294                 return buffer.length;
 295             } catch (IOException ex) {
 296                 logger.finest(format("%s exception", this), ex);
 297                 didFail(0, "I/O error");
 298                 return 0;
 299             }
 300         } else {
 301             logger.finest("{0} not connected", this);
 302             didFail(0, "Not connected");
 303             return 0;
 304         }
 305     }
 306 
 307     private void fwkClose() {
 308         synchronized (this) {
 309             logger.finest("{0}", this);
 310             state = State.CLOSE_REQUESTED;
 311             try {
 312                 if (socket != null) {
 313                     socket.close();
 314                 }
 315             } catch (IOException ignore) {}
 316         }
 317     }
 318 
 319     private void fwkNotifyDisposed() {
 320         logger.finest("{0}", this);
 321         state = State.DISPOSED;
 322     }
 323 
 324     private void didOpen() {
 325         Invoker.getInvoker().postOnEventThread(() -> {
 326             if (state == State.ACTIVE) {
 327                 notifyDidOpen();
 328             }
 329         });
 330     }
 331 
 332     private void didReceiveData(final byte[] buffer, final int len) {
 333         Invoker.getInvoker().postOnEventThread(() -> {
 334             if (state == State.ACTIVE) {
 335                 notifyDidReceiveData(buffer, len);
 336             }
 337         });
 338     }
 339 
 340     private void didFail(final int errorCode, final String errorDescription) {
 341         Invoker.getInvoker().postOnEventThread(() -> {
 342             if (state == State.ACTIVE) {
 343                 notifyDidFail(errorCode, errorDescription);
 344             }
 345         });
 346     }
 347 
 348     private void didClose() {
 349         Invoker.getInvoker().postOnEventThread(() -> {
 350             if (state != State.DISPOSED) {
 351                 notifyDidClose();
 352             }
 353         });
 354     }
 355 
 356     private void notifyDidOpen() {
 357         logger.finest("{0}", this);
 358         twkDidOpen(data);
 359     }
 360 
 361     private void notifyDidReceiveData(byte[] buffer, int len) {
 362         if (logger.isLoggable(Level.FINEST)) {
 363             logger.finest(format("%s, len: [%d], data:%s",
 364                     this, len, dump(buffer, len)));
 365         }
 366         twkDidReceiveData(buffer, len, data);
 367     }
 368 
 369     private void notifyDidFail(int errorCode, String errorDescription) {
 370         if (logger.isLoggable(Level.FINEST)) {
 371             logger.finest(format("%s, errorCode: %d, "
 372                     + "errorDescription: %s",
 373                     this, errorCode, errorDescription));
 374         }
 375         twkDidFail(errorCode, errorDescription, data);
 376     }
 377 
 378     private void notifyDidClose() {
 379         logger.finest("{0}", this);
 380         twkDidClose(data);
 381     }
 382 
 383     private static native void twkDidOpen(long data);
 384     private static native void twkDidReceiveData(byte[] buffer, int len,
 385                                                  long data);
 386     private static native void twkDidFail(int errorCode,
 387                                           String errorDescription, long data);
 388     private static native void twkDidClose(long data);
 389 
 390     private static String dump(byte[] buffer, int len) {
 391         StringBuilder sb = new StringBuilder();
 392         int i = 0;
 393         while (i < len) {
 394             StringBuilder c1 = new StringBuilder();
 395             StringBuilder c2 = new StringBuilder();
 396             for (int k = 0; k < 16; k++, i++) {
 397                 if (i < len) {
 398                     int b = buffer[i] & 0xff;
 399                     c1.append(format("%02x ", b));
 400                     c2.append((b >= 0x20 && b <= 0x7e) ? (char) b : '.');
 401                 } else {
 402                     c1.append("   ");
 403                 }
 404             }
 405             sb.append(format("%n  ")).append(c1).append(' ').append(c2);
 406         }
 407         return sb.toString();
 408     }
 409 
 410     @Override
 411     public String toString() {
 412         return format("SocketStreamHandle{host=%s, port=%d, ssl=%s, "
 413                 + "data=0x%016X, state=%s, connected=%s}",
 414                 host, port, ssl, data, state, connected);
 415     }
 416 
 417     private static final class CustomThreadFactory implements ThreadFactory {
 418         private final ThreadGroup group;
 419         private final AtomicInteger index = new AtomicInteger(1);
 420 
 421         private CustomThreadFactory() {
 422             SecurityManager sm = System.getSecurityManager();
 423             group = (sm != null) ? sm.getThreadGroup()
 424                     : Thread.currentThread().getThreadGroup();
 425         }
 426 
 427         @Override
 428         public Thread newThread(Runnable r) {
 429             Thread t = new Thread(group, r, "SocketStreamHandle-"
 430                     + index.getAndIncrement());
 431             t.setDaemon(true);
 432             if (t.getPriority() != Thread.NORM_PRIORITY) {
 433                 t.setPriority(Thread.NORM_PRIORITY);
 434             }
 435             return t;
 436         }
 437     }
 438 }