/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/xmpp/XmppRequest.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.xmpp;

import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import com.caucho.bam.broker.Broker;
import com.caucho.env.thread.ThreadPool;
import com.caucho.hemp.broker.HempBrokerManager;
import com.caucho.network.listen.AbstractProtocolConnection;
import com.caucho.network.listen.SocketLinkDuplexController;
import com.caucho.network.listen.SocketLinkDuplexListener;
import com.caucho.network.listen.TcpSocketLink;
import com.caucho.util.Base64;
import com.caucho.util.L10N;
import com.caucho.util.RandomUtil;
import com.caucho.vfs.IOExceptionWrapper;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.WriteStream;

/**
 * XMPP protocol
 */
public class XmppRequest extends AbstractProtocolConnection {
  private static final L10N L = new L10N(XmppRequest.class);
  private static final Logger log
    = Logger.getLogger(XmppRequest.class.getName());

  private static final String STREAMS_NS = "http://etherx.jabber.org/streams";
  private static final String STARTTLS_NS = "urn:ietf:params:xml:ns:xmpp-tls";
  private static final String AUTH_NS = "urn:ietf:params:xml:ns:xmpp-sasl";

  private XmppProtocol _protocol;

  private HempBrokerManager _brokerManager;
  private Broker _broker;
  
  private TcpSocketLink _conn;

  private ReadStream _is;
  private WriteStream _os;

  private volatile int _requestId;

  private String _id;

  private String _host; // hostname given in stream
  private String _clientTo;
  private String _uid;
  
  private String _streamFrom;
  private String _clientBind;

  private String _name;
  
  private XmppStreamReader _in;

  private boolean _isAllowTls = false;
  private boolean _isRequireSession = true;

  private boolean _isPresent;
  private boolean _isThread;

  private final ThreadPool _threadPool;
  private final BlockingQueue<Stanza> _outboundQueue
    = new ArrayBlockingQueue<Stanza>(1024);

  private State _state;
  private boolean _isFinest;

  XmppRequest(XmppProtocol protocol, TcpSocketLink conn)
  {
    _protocol = protocol;
    _brokerManager = protocol.getBrokerManager();
    _conn = conn;
    _threadPool = ThreadPool.getThreadPool();
  }

  int getRequestId()
  {
    return _requestId;
  }

  String getUid()
  {
    return _uid;
  }

  /**
   * Returns the tcp connection
   */
  public TcpSocketLink getConnection()
  {
    return _conn;
  }

  XmppProtocol getProtocol()
  {
    return _protocol;
  }
  
  /**
   * Initialize the connection.  At this point, the current thread is the
   * connection thread.
   */
  public void init()
  {
  }

  /**
   * Return true if the connection should wait for a read before
   * handling the request.
   */
  public boolean isWaitForRead()
  {
    return true;
  }

  /**
   * Called when the connection starts.
   */
  public void startConnection()
  {
    _host = null;
    _broker = null;
  }
  
  /**
   * Handles a new connection.  The controlling TcpServer may call
   * handleConnection again after the connection completes, so 
   * the implementation must initialize any variables for each connection.
   */
  public boolean handleRequest()
    throws IOException
  {
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();
    
    try {
      thread.setContextClassLoader(_protocol.getClassLoader());

      _isFinest = log.isLoggable(Level.FINEST);
      
      if (_state == null) {
        return handleInit();
      }

      SocketLinkDuplexListener handler
        = new XmppBrokerStream(this, _broker, _is, _in, _os);
      
      SocketLinkDuplexController controller = _conn.startDuplex(handler);

      return true;
    } catch (XMLStreamException e) {
      e.printStackTrace();
      throw new IOExceptionWrapper(e);
    } catch (IOException e) {
      e.printStackTrace();
      throw e;
    } catch (RuntimeException e) {
      e.printStackTrace();
      throw e;
    } finally {
      thread.setContextClassLoader(oldLoader);
    }
  }

  private boolean handleInit()
    throws IOException, XMLStreamException
  {
    _state = State.INIT;

    StringBuilder sb = new StringBuilder();
    Base64.encode(sb, RandomUtil.getRandomLong());
    while (sb.charAt(sb.length() - 1) == '=')
      sb.setLength(sb.length() - 1);
    
    _id = sb.toString();
    
    int ch;
      
    _is = _conn.getReadStream();
    _os = _conn.getWriteStream();
      
    _in = new XmppStreamReaderImpl(_is, _protocol.getMarshalFactory());

    if (! readStreamHeader())
      return false;

    writeStreamHeader(_host);

    readStreamInit();

    return true;
  }

  private boolean readStreamHeader()
    throws IOException, XMLStreamException
  {
    int tag;
    while ((tag = _in.next()) > 0
           && tag != XMLStreamConstants.START_ELEMENT) {
      if (_isFinest)
        debug(_in);
    }
    
    if (_isFinest)
      debug(_in);

    String name = _in.getLocalName();
      
    if (! "stream".equals(name)) {
      _os.print("<error><invalid-xml/></error>");
      
      if (log.isLoggable(Level.FINE))
        log.fine(L.l("{0}: '{1}' is an unknown tag from {2}",
                     this, name, _conn.getRemoteAddress()));
      return false;
    }
    else if (! STREAMS_NS.equals(_in.getNamespaceURI())) {
      _os.print("<error><bad-namespace-prefix/></error>");
      
      if (log.isLoggable(Level.FINE))
        log.fine(L.l("{0}: xmlns='{1}' is an unknown namespace from {2}",
                     this, name, _conn.getRemoteAddress()));
      
      return false;
    }
    /*
    else if (! "jabber:client".equals(_in.getNamespaceURI(""))) {
      _os.print("<error><bad-namespace-prefix/></error>");
      
      if (log.isLoggable(Level.FINE))
        log.fine(L.l("{0}: xmlns='{1}' is an unknown namespace for '' from {2}",
                     this, name, _conn.getRemoteAddress()));
      
      return false;
    }
    */
    else if (! "1.0".equals(_in.getAttributeValue(null, "version"))) {
      _os.print("<error><unsupported-version/></error>");
      
      if (log.isLoggable(Level.FINE))
        log.fine(L.l("{0}: version='{1}' is an unknown version from {2}",
                     this, _in.getAttributeValue(null, "version"),
                     _conn.getRemoteAddress()));
      
      return false;
    }

    _host = _in.getAttributeValue(null, "to");

    String from = _host;

    if (from == null)
      from = _conn.getLocalAddress().getHostAddress();

    _broker = _brokerManager.findBroker(_host);

    if (_broker == null) {
      if (log.isLoggable(Level.FINE))
        log.fine(L.l("{0}: host='{1}' is an unknown host",
                     this, _host));

      _os.print("<error><unknown-host/></error>");
      
      return false;
    }
      
    _streamFrom = from;
    _clientTo = from + "/" + _id;

    return true;
  }

  private boolean skipToStartElement()
    throws IOException, XMLStreamException
  {
    int tag;
    
    while ((tag = _in.next()) > 0
           && tag != XMLStreamConstants.START_ELEMENT) {
      if (_isFinest)
        debug(_in);
    }
    
    if (tag >= 0 && _isFinest)
      debug(_in);

    return tag >= 0;
  }
  
  private boolean readStreamInit()
    throws IOException, XMLStreamException
  {
    if (! skipToStartElement())
      return false;

    if ("starttls".equals(_in.getLocalName())
        && STARTTLS_NS.equals(_in.getNamespaceURI())) {
      if (! startTls())
        return false;
    }

    if ("auth".equals(_in.getLocalName())
        && AUTH_NS.equals(_in.getNamespaceURI())) {
      if (! handleAuth())
        return false;

      if (! skipToStartElement())
        return false;
    }

    if ("stream".equals(_in.getLocalName())
        && STREAMS_NS.equals(_in.getNamespaceURI())) {
      if (! handleStream())
        return false;
    }

    return true;
  }

  private boolean startTls()
    throws IOException, XMLStreamException
  {
    skipToEnd("starttls");

    _os.print("<proceed xmlns='" + STARTTLS_NS + "'/>");
    _os.flush();

    // _conn.upgradeTLS();
      
    System.out.println("STARTTLS");

    return true;
  }

  private void writeStreamHeader(String host)
    throws IOException
  {
    _os.print("<stream:stream xmlns='jabber:client'");
    _os.print(" xmlns:stream='http://etherx.jabber.org/streams'");
    _os.print(" id='" + _id + "'");
    _os.print(" from='" + host + "'");
    _os.print(" version='1.0'>");
      
    // + "   <mechanism>DIGEST-MD5</mechanism>\n"
    _os.print("<stream:features>");
    
    _os.print("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>");
    _os.print("<mechanism>PLAIN</mechanism>");
    _os.print("</mechanisms>");

    if (_isAllowTls)
      _os.print("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'></starttls>");
    
    _os.print("<auth xmlns='http://jabber.org/features/iq-auth'></auth>");
    //_os.print("<register xmlns='http://jabber.org/features/iq-register'></register>");
    _os.print("</stream:features>\n");
    _os.flush();
  }

  private boolean handleStream()
    throws IOException, XMLStreamException
  {
    String name = _in.getLocalName();

    String to = null;
      
    for (int i = _in.getAttributeCount() - 1; i >= 0; i--) {
      String localName = _in.getAttributeLocalName(i);
      String value = _in.getAttributeValue(i);

      if ("to".equals(localName))
        to = value;
    }

    String from = _host;

    if (from == null)
      from = to;

    if (from == null)
      from = _conn.getLocalAddress().getHostAddress();

    if (log.isLoggable(Level.FINE))
      log.fine(this + " stream open(from=" + from + " id=" + _id + ")");
      
    _os.print("<stream:stream xmlns='jabber:client'");
    _os.print(" xmlns:stream='http://etherx.jabber.org/streams'");
    _os.print(" id='" + _id + "'");
    _os.print(" from='" + from + "'");
    _os.print(" version='1.0'>");
      
    _os.print("<stream:features>");
    _os.print("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>");

    if (_isRequireSession)
      _os.print("<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>");
    
    _os.print("</stream:features>");
    _os.flush();

    return true;
  }

  private void skipToEnd(String tagName)
    throws IOException, XMLStreamException
  {
    XMLStreamReader in = _in;
      
    if (in == null)
      return;

    int tag;
    while ((tag = in.next()) > 0) {
      if (_isFinest)
        debug(in);

      if (tag == XMLStreamReader.START_ELEMENT) {
      }
      else if (tag == XMLStreamReader.END_ELEMENT) {
        if (tagName.equals(in.getLocalName()))
          return;
      }
    }
  }

  private boolean handleAuth()
    throws IOException, XMLStreamException
  {
    String mechanism = _in.getAttributeValue(null, "mechanism");

    if ("PLAIN".equals(mechanism))
      return handleAuthPlain();
    else
      throw new IllegalStateException("Unknown mechanism: " + mechanism);
  }

  private boolean handleAuthPlain()
    throws IOException, XMLStreamException
  {
    String value = null;

    int tag;
    while ((tag = _in.next()) > 0
           && tag != XMLStreamConstants.START_ELEMENT
           && tag != XMLStreamConstants.END_ELEMENT) {
      if (_isFinest)
        debug(_in);
      
      if (tag == XMLStreamConstants.CHARACTERS) {
        char []buffer = _in.getTextCharacters();
        int start = _in.getTextStart();
        int len = _in.getTextLength();

        value = new String(_in.getTextCharacters(), start, len);
      }
    }

    if (value == null)
      return false;
    
    if (_isFinest)
      debug(_in);

    String decoded = Base64.decode(value);

    int p = decoded.indexOf(0, 1);

    if (p < 0)
      return false;

    String name = decoded.substring(1, p);
    String password = decoded.substring(p + 1);

    boolean isAuth = true;

    if (isAuth) {
      _name = name;

      _uid = _name + "@" + _host;

      if (log.isLoggable(Level.FINE))
        log.fine(this + " auth-plain success for " + name);
      
      _os.print("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'></success>");
      _os.flush();
      
      return true;
    }

    return false;
  }

  private void debug(XMLStreamReader in)
    throws IOException, XMLStreamException
  {
    if (XMLStreamReader.START_ELEMENT == in.getEventType()) {
      StringBuilder sb = new StringBuilder();
      sb.append("<").append(in.getLocalName());

      if (in.getNamespaceURI() != null)
        sb.append("{").append(in.getNamespaceURI()).append("}");

      for (int i = 0; i < in.getAttributeCount(); i++) {
        sb.append(" ");
        sb.append(in.getAttributeLocalName(i));
        sb.append("='");
        sb.append(in.getAttributeValue(i));
        sb.append("'");
      }
      sb.append(">");

      log.finest(this + " " + sb);
    }
    else if (XMLStreamReader.END_ELEMENT == in.getEventType()) {
      log.finest(this + " </" + in.getLocalName() + ">");
    }
    else if (XMLStreamReader.CHARACTERS == in.getEventType()) {
      String text = in.getText().trim();

      if (! "".equals(text))
        log.finest(this + " text='" + text + "'");
    }
    else
      log.finest(this + " tag=" + in.getEventType());
  }
  
  /**
   * Resumes processing after a wait.
   */
  public boolean handleResume()
    throws IOException
  {
    return false;
  }

  /**
   * Handles a close event when the connection is closed.
   */
  public void onCloseConnection()
  {
    _requestId++;
    
    _state = null;
    _isPresent = false;
  }

  @Override
  public String toString()
  {
    if (_conn != null)
      return getClass().getSimpleName() + "[" + _conn.getId() + "]";
    else
      return getClass().getSimpleName() + "[]";
  }

  enum State {
    INIT
  };
}
