/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/remote/websocket/WebSocketClient.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.remote.websocket;

import com.caucho.util.*;
import com.caucho.vfs.*;
import com.caucho.websocket.WebSocketContext;
import com.caucho.websocket.WebSocketListener;

import java.io.*;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.logging.*;

/**
 * WebSocketClient
 */
public class WebSocketClient implements WebSocketContext, WebSocketConstants {
  private static final Logger log
    = Logger.getLogger(WebSocketClient.class.getName());
  private static final L10N L = new L10N(WebSocketClient.class);
  
  private String _url;

  private String _scheme;
  private String _host;
  private int _port;
  private String _path;
  
  private String _virtualHost;
  private boolean _isMasked;

  private WebSocketListener _listener;

  private Socket _s;
  private ReadStream _is;
  private WriteStream _os;
  private boolean _isClosed;

  private ClientContext _context;
  
  private FrameInputStream _frameIs;
  private WebSocketInputStream _wsIs;
  private WebSocketOutputStream _wsOs;
  
  public WebSocketClient(String url,
                         WebSocketListener listener)
  {
    setUrl(url);
    
    _listener = listener;
    
    if (url == null)
      throw new IllegalArgumentException();
    
    if (_listener == null)
      throw new IllegalArgumentException();
  }
  
  private void setUrl(String url)
  {
    _url = url;
    parseUrl(url);
  }
  
  public void setVirtualHost(String virtualHost)
  {
    _virtualHost = virtualHost;
  }
  
  public void setMasked(boolean isMasked)
  {
    _isMasked = isMasked;
  }

  public void connect()
    throws IOException
  {
    connect(null, null);
  }

  public void connect(String userName, String password)
    throws IOException
  {
    if (_s != null)
      return;
    
    connectImpl(userName, password);
  }

  private void parseUrl(String url)
  {
    int p = url.indexOf("://");
    if (p < 0)
      throw new IllegalArgumentException(L.l("'{0}' is an illegal URL because it is missing a scheme",
                                             url));

    _scheme = url.substring(0, p);

    int q = url.indexOf('/', p + 3);

    String server;
    if (q < 0) {
      server = url.substring(p + 3);
      _path = "/";
    }
    else {
      server = url.substring(p + 3, q);
      _path = url.substring(q);
    }

    p = server.indexOf(':');

    if (p < 0) {
      _host = server;
      _port = 80;
    }
    else {
      _host = server.substring(0, p);
      _port = Integer.parseInt(server.substring(p + 1));
    }
  }

  protected void connectImpl(String userName, String password)
    throws IOException
  {
    if (_listener == null)
      throw new IllegalStateException("missing websocket listener");
    
    _s = new Socket(_host, _port);

    _is = Vfs.openRead(_s.getInputStream());
    _os = Vfs.openWrite(_s.getOutputStream());

    _os.print("GET " + _path + " HTTP/1.1\r\n");
    
    if (_virtualHost != null)
      _os.print("Host: " + _virtualHost + "\r\n");
    else if (_host != null)
      _os.print("Host: " + _host + "\r\n");
    else
      _os.print("Host: localhost\r\n");
    
    byte []clientNonce = new byte[16];
    
    String key = Base64.encode(clientNonce);
    
    _os.print("Sec-WebSocket-Key: " + key + "\r\n");
    
    String version = WebSocketConstants.VERSION;
    
    _os.print("Sec-WebSocket-Version: " + version + "\r\n");
      
    if (! _isMasked)
      _os.print("Sec-WebSocket-Extensions: x-unmasked\r\n");
    
    _os.print("Upgrade: WebSocket\r\n");
    _os.print("Connection: Upgrade\r\n");
    
    _os.print("\r\n");
    _os.flush();

    parseHeaders(_is);

    _frameIs = new UnmaskedFrameInputStream();
    _frameIs.init(_is);
    
    // _wsOut = new WebSocketOutputStream(_out);
    // _wsIn = new WebSocketInputStream(_is);
    
    _os.flush();
    
    _context = new ClientContext();

    _listener.onStart(this);
    
    // XXX: ThreadPool?
    Thread thread = new Thread(_context);
    thread.setDaemon(true);
    thread.start();
  }

  protected void parseHeaders(ReadStream in)
    throws IOException
  {
    String status = in.readln();

    if (! status.startsWith("HTTP")) {
      throw new IOException(L.l("Unexpected response {0}", status));
    }
    
    String line;
    while ((line = in.readln()) != null && line.length() != 0) {
      int p = line.indexOf(':');

      if (p > 0) {
        String header = line.substring(0, p).trim();
        String value = line.substring(p + 1).trim();
      }
    }

    if (! status.startsWith("HTTP/1.1 101")) {
      StringBuilder sb = new StringBuilder();
      
      int ch;
      
      while (in.available() > 0 && (ch = in.read()) >= 0) {
        sb.append((char) ch);
      }
      
      throw new IOException(L.l("Unexpected response {0}\n\n{1}",
                                status, sb));
      
    }
  }
 
  private int scanToLf(byte []data)
  {
    for (int i = 0; i < data.length; i++) {
      if (data[i] == '\n')
        return i;
    }
    
    return -1;
  }
  
  @Override
  public void disconnect()
  {
    OutputStream os = _os;

    try {
      if (os != null)
        os.close();
    } catch (IOException e) {
      log.log(Level.WARNING, e.toString(), e);
    }
  }
  
  public boolean isClosed()
  {
    return _isClosed;
  }

  public void close()
  {
    _isClosed = true;
    
    OutputStream os = _os;
    _os = null;

    InputStream is = _is;
    _is = null;

    Socket s = _s;
    _s = null;

    try {
      if (os != null)
        os.close();
    } catch (IOException e) {
      log.log(Level.WARNING, e.toString(), e);
    }

    try {
      if (is != null)
        is.close();
    } catch (IOException e) {
      log.log(Level.WARNING, e.toString(), e);
    }

    try {
      if (s != null)
        s.close();
    } catch (IOException e) {
      log.log(Level.WARNING, e.toString(), e);
    }
  }
  public InputStream getInputStream()
  {
    return _is;
  }

  @Override
  public OutputStream startBinaryMessage()
    throws IOException
  {
    if (_wsOs == null) {
      OutputStream os = _os;
      if (os == null)
        throw new IllegalStateException(L.l("startBinaryMessage cannot be called with a closed context"));
      
      _wsOs = new WebSocketOutputStream(os, new byte[4096]);
    }
    
    _wsOs.init();
    
    return _wsOs;
  }

  public PrintWriter startTextMessage()
  {
    throw new UnsupportedOperationException();
  }

  public long getTimeout()
  {
    return 0;
  }

  public void setTimeout(long timeout)
  {
  }


  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _url + "]";
  }

  class ClientContext implements Runnable
  {
    public void run()
    {
      Thread thread = Thread.currentThread();
      String name = thread.getName();
      
      try {
        thread.setName("web-socket-client");
        
        handleRequests();
      } catch (Exception e) {
        if (_isClosed)
          log.log(Level.FINEST, e.toString(), e);
        else
          log.log(Level.WARNING, e.toString(), e);
      } finally {
        try {
          _listener.onDisconnect(WebSocketClient.this);
        } catch (IOException e) {
          log.log(Level.WARNING, e.toString(), e);
        }
        
        close();
        
        thread.setName(name);
      }
    }

    public void handleRequests()
      throws IOException
    {
      // server/2h20
      // _listener.onStart(this);
      
      FrameInputStream is = _frameIs;

      while (is.readFrameHeader()) {
        
        int op = is.getOpcode();
        
        switch (op) {
        case OP_BINARY:
          if (_wsIs == null)
            _wsIs = new WebSocketInputStream(is);
          
          _wsIs.init();

          _listener.onReadBinary(WebSocketClient.this, _wsIs);
          break;
          
        default:
          throw new IllegalStateException("Unknown WebSocket opcode 0x" +
                                          Integer.toHexString(op));
        }
      }
    }
  }
}
