/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.cert.Certificate;
import java.net.InetSocketAddress;
import javax.xml.XMLConstants;
import java.io.StringReader;
import java.util.Locale;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.net.VirtualConnection;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.PacketDeliverer;
import org.jivesoftware.openfire.nio.OfflinePacketDeliverer;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.websocket.*;
import org.jivesoftware.openfire.multiplex.UnknownStanzaException;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SASLAuthentication.Status;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.openfire.plugin.ofmeet.jetty.OfMeetLoginService;
import org.jivesoftware.openfire.plugin.rest.RestEventSourceServlet;
import java.util.*;
import java.util.concurrent.*;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.PacketParserUtils;
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
import javax.security.auth.callback.*;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.xmlpull.mxp1.MXParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmpp.packet.*;
import org.dom4j.*;
import org.dom4j.io.XMPPPacketReader;
public class SmackConnection extends VirtualConnection
{
private static Logger Log = LoggerFactory.getLogger( "SmackConnection" );
private static final String STREAM_HEADER = "open";
private static final String STREAM_FOOTER = "close";
private static final String FRAMING_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-framing";
private SessionPacketRouter router;
private LocalClientSession xmppSession;
private boolean startedSASL = false;
private Status saslStatus;
private String remoteAddr;
private String hostName;
private boolean isSecure = false;
private XMPPConnection.OpenfirePacketReader reader;
private XMPPConnection.OpenfirePacketWriter writer;
private String username;
private PacketDeliverer backupDeliverer;
private static GenericObjectPool<XMPPPacketReader> readerPool;
public SmackConnection(String hostName, XMPPConnection.OpenfirePacketWriter writer, XMPPConnection.OpenfirePacketReader reader)
{
this.remoteAddr = hostName;
this.hostName = hostName;
this.reader = reader;
if (readerPool == null) {
initializePool();
}
}
public void setReader(XMPPConnection.OpenfirePacketReader reader) {
this.reader = reader;
}
public void setWriter(XMPPConnection.OpenfirePacketWriter writer) {
this.writer = writer;
}
public boolean isSecure() {
return isSecure;
}
public void setSecure(boolean isSecure) {
this.isSecure = isSecure;
}
public SessionPacketRouter getRouter()
{
return router;
}
public void setRouter(SessionPacketRouter router, String username)
{
this.router = router;
this.username = username;
}
public void setUsername(String username)
{
this.username = username;
}
public void closeVirtualConnection()
{
Log.info("SmackConnection - close ");
if (this.reader!= null) this.reader.shutdown();
if (this.writer!= null) this.writer.shutdown();
if (this.reader!= null) this.reader.cleanup();
if (this.writer!= null) this.writer.cleanup();
}
public byte[] getAddress() {
return remoteAddr.getBytes();
}
public String getHostAddress() {
return remoteAddr;
}
public String getHostName() {
return ( hostName != null ) ? hostName : "0.0.0.0";
}
public void systemShutdown() {
deliverRawText(new StreamError(StreamError.Condition.system_shutdown).toXML());
close();
}
public void deliver(org.xmpp.packet.Packet packet) throws UnauthorizedException
{
final String xml;
if (Namespace.NO_NAMESPACE.equals(packet.getElement().getNamespace()))
{
// use string-based operation here to avoid cascading xmlns wonkery
StringBuilder packetXml = new StringBuilder(packet.toXML());
packetXml.insert(packetXml.indexOf(" "), " xmlns=\"jabber:client\"");
xml = packetXml.toString();
} else {
xml = packet.toXML();
}
if (validate()) {
deliverRawText(xml);
} else {
// use fallback delivery mechanism (offline)
getPacketDeliverer().deliver(packet);
}
}
void deliverToSSE(String packet)
{
RestEventSourceServlet.emitEvent("chatapi.xmpp", username, packet);
}
public void deliverRawText(String text)
{
Log.info("SmackConnection - deliverRawText\n" + text);
try {
deliverToSSE(text);
StringReader stringReader = new StringReader(text);
XmlPullParser parser = new MXParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(stringReader);
int eventType = parser.getEventType();
do {
if (eventType == XmlPullParser.START_TAG)
{
if (parser.getName().equals("message")) {
this.reader.processPacket(PacketParserUtils.parseMessage(parser));
}
else if (parser.getName().equals("iq")) {
this.reader.processPacket(PacketParserUtils.parseIQ(parser, reader.connection));
}
else if (parser.getName().equals("presence")) {
this.reader.processPacket(PacketParserUtils.parsePresence(parser));
}
}
else if (eventType == XmlPullParser.END_TAG) {
}
eventType = parser.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
}
catch (Exception e) {
Log.error("deliverRawText error", e);
}
}
@Override
public boolean validate() {
return router != null;
}
@Override
public PacketDeliverer getPacketDeliverer() {
if (backupDeliverer == null) {
backupDeliverer = new OfflinePacketDeliverer();
}
return backupDeliverer;
}
@Override
public org.jivesoftware.openfire.spi.ConnectionConfiguration getConfiguration()
{
// TODO Here we run into an issue with the ConnectionConfiguration introduced in Openfire 4:
// it is not extensible in the sense that unforeseen connection types can be added.
// For now, null is returned, as this object is likely to be unused (its lifecycle is
// not managed by a ConnectionListener instance).
return null;
}
public Certificate[] getPeerCertificates() {
return null;
}
public void sendSmackXmppMessage(String stanza)
{
Log.debug("sendSmackXmppMessage\n" + stanza);
XMPPPacketReader reader = null;
try {
reader = readerPool.borrowObject();
Document doc = reader.read(new StringReader(stanza));
if (router == null) {
if (isStreamManagementAvailable()) {
router = new StreamManagementPacketRouter(xmppSession);
} else {
// fall back for older Openfire installations
router = new SessionPacketRouter(xmppSession);
}
}
router.route(doc.getRootElement());
} catch (Exception ex) {
Log.error("Failed to process smack XMPP stanza", ex);
} finally {
if (reader != null) {
readerPool.returnObject(reader);
}
}
}
public void sendRawXmppMessage(String stanza)
{
Log.debug("sendRawXmppMessage\n" + stanza);
XMPPPacketReader reader = null;
try {
reader = readerPool.borrowObject();
Document doc = reader.read(new StringReader(stanza));
if (xmppSession == null) {
initiateSession(doc.getRootElement());
} else {
processStanza(doc.getRootElement());
}
} catch (Exception ex) {
Log.error("Failed to process raw XMPP stanza", ex);
} finally {
if (reader != null) {
readerPool.returnObject(reader);
}
}
}
/*
* Process stream headers/footers and authentication stanzas locally;
* otherwise delegate stanza handling to the session packet router.
*/
private void processStanza(Element stanza) {
try {
String tag = stanza.getName();
if (STREAM_FOOTER.equals(tag)) {
closeStream(null);
} else if ("auth".equals(tag)) {
// User is trying to authenticate using SASL
startedSASL = true;
// Process authentication stanza
xmppSession.incrementClientPacketCount();
saslStatus = SASLAuthentication.handle(xmppSession, stanza);
} else if (startedSASL && "response".equals(tag) || "abort".equals(tag)) {
// User is responding to SASL challenge. Process response
xmppSession.incrementClientPacketCount();
saslStatus = SASLAuthentication.handle(xmppSession, stanza);
} else if (STREAM_HEADER.equals(tag)) {
// restart the stream
openStream(stanza.attributeValue(QName.get("lang", XMLConstants.XML_NS_URI), "en"), stanza.attributeValue("from"));
configureStream();
} else if (Status.authenticated.equals(saslStatus)) {
if (router == null) {
if (isStreamManagementAvailable()) {
router = new StreamManagementPacketRouter(xmppSession);
} else {
// fall back for older Openfire installations
router = new SessionPacketRouter(xmppSession);
}
}
router.route(stanza);
} else {
// require authentication
Log.warn("Not authorized: " + stanza.asXML());
sendPacketError(stanza, PacketError.Condition.not_authorized);
}
} catch (UnknownStanzaException use) {
Log.warn("Received invalid stanza: " + stanza.asXML());
sendPacketError(stanza, PacketError.Condition.bad_request);
} catch (Exception ex) {
Log.error("Failed to process incoming stanza: " + stanza.asXML(), ex);
closeStream(new StreamError(StreamError.Condition.internal_server_error));
}
}
/*
* Initiate the stream and corresponding XMPP session.
*/
private void initiateSession(Element stanza) {
String host = stanza.attributeValue("to");
StreamError streamError = null;
Locale language = Locale.forLanguageTag(stanza.attributeValue(QName.get("lang", XMLConstants.XML_NS_URI), "en"));
if (STREAM_FOOTER.equals(stanza.getName())) {
// an error occurred while setting up the session
Log.warn("Client closed stream before session was established");
} else if (!STREAM_HEADER.equals(stanza.getName())) {
streamError = new StreamError(StreamError.Condition.unsupported_stanza_type);
Log.warn("Closing session due to incorrect stream header. Tag: " + stanza.getName());
} else if (!FRAMING_NAMESPACE.equals(stanza.getNamespace().getURI())) {
// Validate the stream namespace (https://tools.ietf.org/html/rfc7395#section-3.3.2)
streamError = new StreamError(StreamError.Condition.invalid_namespace);
Log.warn("Closing session due to invalid namespace in stream header. Namespace: " + stanza.getNamespace().getURI());
} else if (!validateHost(host)) {
streamError = new StreamError(StreamError.Condition.host_unknown);
Log.warn("Closing session due to incorrect hostname in stream header. Host: " + host);
} else {
// valid stream; initiate session
xmppSession = SessionManager.getInstance().createClientSession(this, language);
xmppSession.setSessionData("sse", Boolean.TRUE);
Log.warn("initiateSession - creating new session (rest_sse) for " + username);
}
if (xmppSession == null) {
closeStream(streamError);
} else {
openStream(language.toLanguageTag(), stanza.attributeValue("from"));
configureStream();
}
}
private boolean validateHost(String host) {
boolean result = true;
if (JiveGlobals.getBooleanProperty("xmpp.client.validate.host", false)) {
result = XMPPServer.getInstance().getServerInfo().getXMPPDomain().equals(host);
}
return result;
}
/*
* Prepare response for stream initiation (sasl) or stream restart (features).
*/
private void configureStream() {
StringBuilder sb = new StringBuilder(250);
sb.append("<stream:features xmlns:stream='http://etherx.jabber.org/streams'>");
if (saslStatus == null) {
// Include available SASL Mechanisms
sb.append(SASLAuthentication.getSASLMechanisms(xmppSession));
if (XMPPServer.getInstance().getIQRouter().supports("jabber:iq:auth")) {
sb.append("<auth xmlns='http://jabber.org/features/iq-auth'/>");
}
} else if (saslStatus.equals(Status.authenticated)) {
// Include Stream features
sb.append(String.format("<bind xmlns='%s'/>", "urn:ietf:params:xml:ns:xmpp-bind"));
sb.append(String.format("<session xmlns='%s'><optional/></session>", "urn:ietf:params:xml:ns:xmpp-session"));
if (isStreamManagementAvailable()) {
sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V3));
}
sendSmackXmppMessage("<presence from=\"" + username + "@" + XMPPServer.getInstance().getServerInfo().getXMPPDomain() + "\" />");
}
sb.append("</stream:features>");
deliverToSSE(sb.toString());
}
private void openStream(String lang, String jid) {
xmppSession.incrementClientPacketCount();
StringBuilder sb = new StringBuilder(250);
sb.append("<open ");
if (jid != null) {
sb.append("to='").append(jid).append("' ");
}
sb.append("from='").append(XMPPServer.getInstance().getServerInfo().getXMPPDomain()).append("' ");
sb.append("id='").append(xmppSession.getStreamID().toString()).append("' ");
sb.append("xmlns='").append(FRAMING_NAMESPACE).append("' ");
sb.append("xml:lang='").append(lang).append("' ");
sb.append("version='1.0'/>");
deliverToSSE(sb.toString());
}
private void closeStream(StreamError streamError)
{
if (streamError != null) {
deliverToSSE(streamError.toXML());
}
StringBuilder sb = new StringBuilder(250);
sb.append("<close ");
sb.append("xmlns='").append(FRAMING_NAMESPACE).append("'");
sb.append("/>");
deliverToSSE(sb.toString());
//RestEventSourceServlet.closeXmpp(username);
}
private void sendPacketError(Element stanza, PacketError.Condition condition) {
Element reply = stanza.createCopy();
reply.addAttribute("type", "error");
reply.addAttribute("to", stanza.attributeValue("from"));
reply.addAttribute("from", stanza.attributeValue("to"));
reply.add(new PacketError(condition).getElement());
deliverToSSE(reply.asXML());
}
private synchronized void initializePool() {
if (readerPool == null) {
readerPool = new GenericObjectPool<XMPPPacketReader>(new XMPPPPacketReaderFactory());
readerPool.setMaxTotal(-1);
readerPool.setBlockWhenExhausted(false);
readerPool.setTestOnReturn(true);
readerPool.setTimeBetweenEvictionRunsMillis(JiveConstants.MINUTE);
}
}
private boolean isStreamManagementAvailable() {
return JiveGlobals.getBooleanProperty(StreamManager.SM_ACTIVE, true);
}
}