/* * 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 }; }