/* * 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.io.Serializable; import java.net.InetAddress; import java.net.Socket; import java.util.HashSet; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import com.caucho.bam.broker.Broker; import com.caucho.bam.stream.AbstractMessageStream; import com.caucho.bam.stream.MessageStream; import com.caucho.env.thread.ThreadPool; 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.SocketStream; import com.caucho.vfs.WriteStream; import com.caucho.xmpp.im.ImBindQuery; /** * XMPP protocol */ public class XmppClient { private static final L10N L = new L10N(XmppClient.class); private static final Logger log = Logger.getLogger(XmppClient.class.getName()); private static final String STREAMS_NS = "http://etherx.jabber.org/streams"; private static final String AUTH = "auth{http://jabber.org/features/iq-auth}"; private static final String REGISTER = "register{http://jabber.org/features/iq-register}"; private InetAddress _inetAddress; private int _port; private String _to; private Socket _s; private ReadStream _is; private WriteStream _os; private String _id; private String _from; private XmppStreamReader _in; private XmppReader _reader; private boolean _isFinest; private int _mId; private HashSet<String> _authMechanisms = new HashSet<String>(); private HashSet<String> _features = new HashSet<String>(); private BlockingQueue<Stanza> _stanzaQueue = new LinkedBlockingQueue<Stanza>(); private XmppContext _xmppContext = new XmppContext(); private XmppClientBrokerStream _toBroker; private BindCallback _bindCallback; private String _address; private MessageStream _callback; public XmppClient(InetAddress address, int port) { _inetAddress = address; _port = port; _to = _inetAddress.getHostAddress(); _isFinest = log.isLoggable(Level.FINEST); } public XmppClient(String address, int port) { this(getByName(address), port); _to = address; } private static InetAddress getByName(String address) { try { return InetAddress.getByName(address); } catch (Exception e) { throw new RuntimeException(e); } } public void connect(String name, String password) throws IOException { connect(); login(name, password); } public void connect() throws IOException { try { if (_s != null) throw new IllegalStateException(L.l("{0} is already connected", this)); _s = new Socket(_address, _port); SocketStream ss = new SocketStream(_s); _os = new WriteStream(ss); _is = new ReadStream(ss); _os.print("<?xml version='1.0' encoding='UTF-8' ?>\n"); _os.setEncoding("utf-8"); startStream(); _os.flush(); XMLInputFactory factory = XMLInputFactory.newInstance(); XmppMarshalFactory marshalFactory = new XmppMarshalFactory(); XmppStreamWriterImpl out; out = new XmppStreamWriterImpl(_os, marshalFactory); XmppWriterImpl writer = new XmppWriterImpl(_xmppContext, out); _toBroker = new XmppClientBrokerStream(this, writer); _in = new XmppStreamReaderImpl(_is, marshalFactory); _bindCallback = new BindCallback(); _reader = new XmppReader(_xmppContext, _is, _in, _toBroker, _bindCallback); String tag = readStartTag(); if (! tag.equals("stream") || ! STREAMS_NS.equals(_in.getNamespaceURI())) { throw new IOExceptionWrapper(L.l("<{0}> with ns={1} is an unexpected server response", tag, _in.getNamespaceURI())); } readStreamFeatures(); } catch (XMLStreamException e) { throw new IOExceptionWrapper(e); } } public void login(String name, String password) throws IOException { String base64 = Base64.encode("" + (char) 0 + name + (char) 0 + password); if (log.isLoggable(Level.FINER)) log.finer(this + " authenticating " + name); _os.print("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"); _os.print(base64); _os.print("</auth>"); startStream(); _os.flush(); try { if (! readSuccess()) throw new RuntimeException("expected success"); if (! readStream()) throw new RuntimeException("expected stream"); StringBuilder sb = new StringBuilder(); Base64.encode(sb, RandomUtil.getRandomLong()); _os.print("<iq type='set' id='" + _mId++ + "'>"); _os.print("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>"); _os.print("<resource>" + getClass().getSimpleName() + "</resource>"); _os.print("</bind>"); _os.print("</iq>"); _os.flush(); _reader.readNext(); if (_address == null) throw new RuntimeException("expected bind"); _os.print("<iq type='set' id='" + _mId++ + "'>"); _os.print("<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>"); _os.print("</iq>"); _os.flush(); _reader.readNext(); /* Stanza stanza = _stanzaQueue.poll(2, TimeUnit.SECONDS); if (! (stanza instanceof SessionStanza) && ! (stanza instanceof EmptyStanza)) throw new RuntimeException("expected session"); */ if (log.isLoggable(Level.FINER)) log.finer(this + " authentication successful for " + name); _reader.setHandler(_callback); ThreadPool.getThreadPool().start(new Listener()); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } public void setCallback(MessageStream callback) { _callback = callback; if (_reader != null && _address != null) _reader.setHandler(callback); } public void send(String type, String to, String body) throws IOException { send(type, to, body, null); } public void send(String type, String to, String body, String subject) throws IOException { if (log.isLoggable(Level.FINER)) log.finer(this + " send to=" + to + " body=" + body); try { _os.print("<message "); _os.print(" type='" + type + "'"); if (to != null) _os.print(" to='" + to + "'"); if (_from != null) _os.print(" from='" + _from + "'"); _os.print(">"); if (subject != null) _os.print("<subject>" + subject + "</subject>"); if (body != null) _os.print("<body>" + body + "</body>"); _os.print("</message>"); _os.flush(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } private void startStream() throws IOException { _os.print("<stream:stream"); _os.print(" to='" + _to + "'"); _os.print(" xmlns='jabber:client'"); _os.print(" xmlns:stream='http://etherx.jabber.org/streams'"); _os.print(" version='1.0'"); _os.print(">"); _os.flush(); if (log.isLoggable(Level.FINER)) log.finer(this + " <stream:stream xmlns='jabber:client' to='" + _to + "'>"); } private void readStreamFeatures() throws IOException, XMLStreamException { String startTag = readStartTag(); if (! "features".equals(startTag)) throw unexpected(); int tag = 0; while ((tag = _in.next()) > 0 && ! (tag == XMLStreamReader.END_ELEMENT && "features".equals(_in.getLocalName()))) { if (_isFinest) debug(_in); if (tag == XMLStreamReader.START_ELEMENT) { String localName = _in.getLocalName(); if ("mechanisms".equals(localName)) { } else if ("mechanism".equals(localName)) { tag = _in.next(); String mechanism = _in.getText(); _authMechanisms.add(mechanism); } else { String feature = localName + "{" + _in.getNamespaceURI() + "}"; if (log.isLoggable(Level.FINER)) log.finer(this + " feature " + feature); _features.add(feature); } } else if (tag == XMLStreamReader.END_ELEMENT) { String localName = _in.getLocalName(); } } } private String readStartTag() throws IOException, XMLStreamException { int tag = 0; while ((tag = _in.next()) > 0 && tag != XMLStreamReader.START_ELEMENT) { if (_isFinest) debug(_in); } if (_isFinest) debug(_in); return _in.getLocalName(); } private IOException unexpected() throws IOException, XMLStreamException { if ("error".equals(_in.getLocalName())) { int tag; while ((tag = _in.next()) > 0 && ! (tag == XMLStreamReader.END_ELEMENT && "error".equals(_in.getLocalName()))) { if (tag == XMLStreamReader.START_ELEMENT) { System.out.println("<" + _in.getLocalName() + ">"); } else if (tag == XMLStreamReader.END_ELEMENT) { System.out.println("</" + _in.getLocalName() + ">"); } } return new IOException(L.l("<error> is unexpected", _in.getLocalName())); } else return new IOException(L.l("<{0}> is unexpected", _in.getLocalName())); } public boolean isClosed() { return _s == null; } public void close() { if (log.isLoggable(Level.FINE)) log.fine(this + " close"); try { Socket s; ReadStream is; WriteStream os; synchronized (this) { s = _s; _s = null; is = _is; _is = null; os = _os; _os = null; } if (os != null) { try { os.close(); } catch (IOException e) {} } if (is != null) { is.close(); } if (s != null) { s.close(); } } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } } private void debug(XMLStreamReader in) throws IOException, XMLStreamException { if (XMLStreamReader.START_ELEMENT == in.getEventType()) { StringBuilder sb = new StringBuilder(); sb.append("<"); if (in.getPrefix() != null && ! "".equals(in.getPrefix())) sb.append(in.getPrefix()).append(":"); sb.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()) log.finest(this + " text='" + in.getText() + "'"); else log.finest(this + " tag=" + in.getEventType()); } private boolean readStream() throws IOException, XMLStreamException { int tag; XMLStreamReader in = _in; if (in == null) return false; while ((tag = in.next()) > 0) { if (_isFinest) debug(in); if (tag == XMLStreamReader.START_ELEMENT) { String localName = in.getLocalName(); if ("stream".equals(localName)) { readStreamFeatures(); return true; } else { log.fine(XmppClient.this + " expected stream at tag <" + _in.getLocalName() + ">"); close(); return false; } } else if (tag == XMLStreamReader.END_ELEMENT) { log.fine(XmppClient.this + " unexpected end </" + _in.getLocalName() + ">"); close(); return false; } } if (tag < 0) { close(); } return false; } private boolean readSuccess() throws IOException, XMLStreamException { int tag; XMLStreamReader in = _in; if (in == null) return false; while ((tag = in.next()) > 0) { if (_isFinest) debug(in); if (tag == XMLStreamReader.START_ELEMENT) { String localName = in.getLocalName(); if ("success".equals(localName)) { _reader.skipToEnd("success"); return true; } else { log.fine(XmppClient.this + " expected success at tag <" + _in.getLocalName() + ">"); close(); return false; } } else if (tag == XMLStreamReader.END_ELEMENT) { log.fine(XmppClient.this + " unexpected end </" + _in.getLocalName() + ">"); close(); return false; } } if (tag < 0) { close(); } return false; } public MessageStream getBrokerStream() { return _toBroker; } @Override public String toString() { return getClass().getSimpleName() + "[" + _address + "," + _port + "]"; } @Override protected void finalize() throws Throwable { super.finalize(); close(); } class BindCallback extends AbstractMessageStream { public String getAddress() { throw new UnsupportedOperationException(); } @Override public Broker getBroker() { throw new UnsupportedOperationException(); } @Override public void queryResult(long id, String to, String from, Serializable value) { if (value instanceof ImBindQuery) { ImBindQuery bind = (ImBindQuery) value; _address = bind.getAddress(); } } } class Listener implements Runnable { private boolean _isFinest; public void run() { _isFinest = log.isLoggable(Level.FINEST); try { while (! isClosed()) { readPacket(); } } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); } finally { close(); } } private void readPacket() throws IOException, XMLStreamException { int tag; XMLStreamReader in = _in; if (in == null) return; while ((tag = in.next()) > 0) { if (_isFinest) debug(in); if (tag == XMLStreamReader.START_ELEMENT) { String localName = in.getLocalName(); if ("success".equals(localName)) { skipToEnd("success"); _stanzaQueue.add(new SuccessStanza(in)); } else if ("stream".equals(localName)) { readStreamFeatures(); _stanzaQueue.add(new StreamStanza(in)); } else if ("iq".equals(localName)) { _reader.handleIq(); } else if ("message".equals(localName)) { _reader.handleMessage(); } else if ("presence".equals(localName)) { _reader.handlePresence(); } else { log.fine(XmppClient.this + " unknown tag <" + _in.getLocalName() + ">"); close(); return; } } else if (tag == XMLStreamReader.END_ELEMENT) { log.fine(XmppClient.this + " unexpected end </" + _in.getLocalName() + ">"); close(); return; } } if (tag < 0) { close(); } } private Stanza readIq(XMLStreamReader in) throws IOException, XMLStreamException { String type = in.getAttributeValue(null, "type"); if ("error".equals(type)) { skipToEnd("iq"); return new IqErrorStanza(in); } else if ("result".equals(type)) { String id = in.getAttributeValue(null, "id"); int tag = in.nextTag(); if (_isFinest) debug(in); if (tag == XMLStreamReader.END_ELEMENT && "iq".equals(in.getLocalName())) { return new EmptyStanza(); } if (tag != XMLStreamReader.START_ELEMENT) throw new IllegalStateException("expected start"); String name = in.getLocalName(); if ("bind".equals(name)) { return readBind(in, id); } else if ("session".equals(name)) { skipToEnd("iq"); return new SessionStanza(); } else { skipToEnd("iq"); return new IqErrorStanza(); } } else { throw new UnsupportedOperationException(type); } } private Stanza readBind(XMLStreamReader in, String id) throws IOException, XMLStreamException { BindStanza bind = new BindStanza(); bind.setId(id); skipToEnd("bind"); skipToEnd("iq"); return bind; } 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; } } } } }