Commit cc20210e authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1339: Merge websocket plugin with Openfire core.

parent c184295a
......@@ -97,7 +97,7 @@ by XMPP clients. The table below details the level of support for the requiremen
<td><a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>: Bidirectional-streams Over Synchronous HTTP (BOSH)</td>
<td class="supported">Yes</td>
</tr><tr>
<td><a href="http://www.xmpp.org/extensions/xep-0206.html">XEP-0206</a>: XMPP Over BOSH</td>
<td><a href="http://www.xmpp.org/extensions/xep-0206.html">XEP-0206</a>: XMPP Over BOSH [<a href="#fn2">2</a>]</td>
<td class="supported">Yes</td>
</tr><tr>
<td><a href="http://www.xmpp.org/extensions/xep-0054.html">XEP-0054</a>: vcard-temp</td>
......@@ -197,11 +197,10 @@ XEPs that only require client-side support are omitted.</p>
<a name="footnotes"></a>
<h2>Footnotes</h2>
[<a name="fn1">1</a>] Support for <u>XEP-0055: Jabber Search</u> is provided by the <a href="http://www.igniterealtime.org/projects/openfire/plugins.jsp">Search plugin</a>.
<br>
<ol>
<li><a name="fn1"/>Support for <u>XEP-0055: Jabber Search</u> is provided by the <a href="http://www.igniterealtime.org/projects/openfire/plugins.jsp">Search plugin</a>.</li>
<li><a name="fn2"/>The implementation also follows the XMPP WebSocket subprotocol (<a href="https://tools.ietf.org/html/rfc7395">RFC 7395</a>) specification, which is a standard extension of the WebSocket protocol specification (<a href="https://tools.ietf.org/html/rfc6455">RFC 6455</a>).</li>
</ol>
</div>
</div>
......
......@@ -40,6 +40,7 @@ import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
import org.jivesoftware.openfire.spi.ConnectionType;
import org.jivesoftware.openfire.spi.EncryptionArtifactFactory;
import org.jivesoftware.openfire.websocket.OpenfireWebSocketServlet;
import org.jivesoftware.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -168,6 +169,7 @@ public final class HttpBindManager implements CertificateEventListener, Property
// Setup the default handlers. Order is important here. First, evaluate if the 'standard' handlers can be used to fulfill requests.
this.handlerList.addHandler( createBoshHandler() );
this.handlerList.addHandler( createWebsocketHandler() );
this.handlerList.addHandler( createCrossDomainHandler() );
// When standard handling does not apply, see if any of the handlers in the extension pool of handlers applies to the request.
......@@ -608,6 +610,25 @@ public final class HttpBindManager implements CertificateEventListener, Property
return context;
}
/**
* Creates a Jetty context handler that can be used to expose Websocket functionality.
*
* Note that an invocation of this method will not register the handler (and thus make the related functionality
* available to the end user). Instead, the created handler is returned by this method, and will need to be
* registered with the embedded Jetty webserver by the caller.
*
* @return A Jetty context handler (never null).
*/
protected Handler createWebsocketHandler()
{
final ServletContextHandler context = new ServletContextHandler( null, "/ws", ServletContextHandler.SESSIONS );
// Add the functionality-providers.
context.addServlet( new ServletHolder( new OpenfireWebSocketServlet() ), "/*" );
return context;
}
// NOTE: enabled by default
private boolean isHttpCompressionEnabled() {
final ConnectionManagerImpl connectionManager = ((ConnectionManagerImpl) XMPPServer.getInstance().getConnectionManager());
......
......@@ -15,12 +15,8 @@
*/
package org.jivesoftware.openfire.websocket;
import java.io.File;
import java.text.MessageFormat;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.common.extensions.compress.PerMessageDeflateExtension;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
......@@ -29,114 +25,66 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginClassLoader;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.http.HttpBindManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This plugin enables XMPP over WebSocket (RFC 7395) for Openfire.
* This Servlet enables XMPP over WebSocket (RFC 7395) for Openfire.
*
* The Jetty WebSocketServlet serves as a base class and enables easy integration into the
* BOSH (http-bind) web context. Each WebSocket request received at the "/ws/" URI will be
* forwarded to this plugin/servlet, which will in turn create a new {@link XmppWebSocket}
* for each new connection.
*/
public class WebSocketPlugin extends WebSocketServlet implements Plugin {
public class OpenfireWebSocketServlet extends WebSocketServlet {
private static final long serialVersionUID = 7281841492829464604L;
private static final Logger Log = LoggerFactory.getLogger(WebSocketPlugin.class);
private static final long serialVersionUID = 7281841492829464605L;
private static final Logger Log = LoggerFactory.getLogger(OpenfireWebSocketServlet.class);
private ServletContextHandler contextHandler;
protected PluginClassLoader pluginClassLoader = null;
@Override
public void initializePlugin(final PluginManager manager, final File pluginDirectory) {
if (Boolean.valueOf(JiveGlobals.getBooleanProperty(HttpBindManager.HTTP_BIND_ENABLED, true))) {
Log.info(String.format("Initializing websocket plugin"));
try {
contextHandler = new ServletContextHandler(null, "/ws", ServletContextHandler.SESSIONS);
contextHandler.addServlet(new ServletHolder(this), "/*");
HttpBindManager.getInstance().addJettyHandler( contextHandler );
} catch (Exception e) {
Log.error("Failed to start websocket plugin", e);
}
} else {
Log.warn("Failed to start websocket plugin; http-bind is disabled");
@Override
public void destroy()
{
// terminate any active websocket sessions
SessionManager sm = XMPPServer.getInstance().getSessionManager();
for (ClientSession session : sm.getSessions()) {
if (session instanceof LocalSession) {
Object ws = ((LocalSession) session).getSessionData("ws");
if (ws != null && (Boolean) ws) {
session.close();
}
}
}
super.destroy();
}
@Override
public void destroyPlugin() {
// terminate any active websocket sessions
SessionManager sm = XMPPServer.getInstance().getSessionManager();
for (ClientSession session : sm.getSessions()) {
if (session instanceof LocalSession) {
Object ws = ((LocalSession) session).getSessionData("ws");
if (ws != null && (Boolean) ws) {
session.close();
}
}
}
HttpBindManager.getInstance().removeJettyHandler( contextHandler );
contextHandler = null;
pluginClassLoader = null;
@Override
public void configure(WebSocketServletFactory factory)
{
if (XmppWebSocket.isCompressionEnabled()) {
factory.getExtensionFactory().register("permessage-deflate", PerMessageDeflateExtension.class);
}
factory.setCreator(new WebSocketCreator() {
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
try {
for (String subprotocol : req.getSubProtocols())
{
if ("xmpp".equals(subprotocol))
{
resp.setAcceptedSubProtocol(subprotocol);
return new XmppWebSocket();
}
}
} catch (Exception e) {
Log.warn(MessageFormat.format("Unable to load websocket factory: {0} ({1})", e.getClass().getName(), e.getMessage()));
}
Log.warn("Failed to create websocket for {}:{} make a request at {}", req.getRemoteAddress(), req.getRemotePort(), req.getRequestPath() );
return null;
}
});
}
@Override
public void configure(WebSocketServletFactory factory)
{
if (XmppWebSocket.isCompressionEnabled()) {
factory.getExtensionFactory().register("permessage-deflate", PerMessageDeflateExtension.class);
}
factory.setCreator(new WebSocketCreator() {
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
try {
ClassLoader pcl = getPluginClassLoader();
Thread.currentThread().setContextClassLoader(pcl == null ? ccl : pcl);
for (String subprotocol : req.getSubProtocols())
{
if ("xmpp".equals(subprotocol))
{
resp.setAcceptedSubProtocol(subprotocol);
return new XmppWebSocket();
}
}
} catch (Exception e) {
Log.warn(MessageFormat.format("Unable to load websocket factory: {0} ({1})", e.getClass().getName(), e.getMessage()));
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
Log.warn("Failed to create websocket: " + req);
return null;
}
});
}
protected synchronized PluginClassLoader getPluginClassLoader() {
PluginManager pm = XMPPServer.getInstance().getPluginManager();
if (pluginClassLoader == null) {
pluginClassLoader = pm.getPluginClassloader(this);
}
// report error if plugin is unavailable
if (pluginClassLoader == null) {
Log.error("Unable to find class loader for websocket plugin");
}
return pluginClassLoader;
}
}
......@@ -41,35 +41,35 @@ import org.jivesoftware.util.JiveGlobals;
*/
public class StreamManagementPacketRouter extends SessionPacketRouter {
public static final String SM_UNSOLICITED_ACK_FREQUENCY = "stream.management.unsolicitedAckFrequency";
static {
JiveGlobals.migrateProperty(SM_UNSOLICITED_ACK_FREQUENCY);
}
private int unsolicitedAckFrequency = JiveGlobals.getIntProperty(SM_UNSOLICITED_ACK_FREQUENCY, 0);
public StreamManagementPacketRouter(LocalClientSession session) {
super(session);
}
public static final String SM_UNSOLICITED_ACK_FREQUENCY = "stream.management.unsolicitedAckFrequency";
static {
JiveGlobals.migrateProperty(SM_UNSOLICITED_ACK_FREQUENCY);
}
@Override
public void route(Element wrappedElement) throws UnknownStanzaException {
if (StreamManager.NAMESPACE_V3.equals(wrappedElement.getNamespace().getStringValue())) {
session.getStreamManager().process( wrappedElement, session.getAddress() );
private int unsolicitedAckFrequency = JiveGlobals.getIntProperty(SM_UNSOLICITED_ACK_FREQUENCY, 0);
public StreamManagementPacketRouter(LocalClientSession session) {
super(session);
}
@Override
public void route(Element wrappedElement) throws UnknownStanzaException {
if (StreamManager.NAMESPACE_V3.equals(wrappedElement.getNamespace().getStringValue())) {
session.getStreamManager().process( wrappedElement, session.getAddress() );
} else {
super.route(wrappedElement);
if (isUnsolicitedAckExpected()) {
session.getStreamManager().sendServerAcknowledgement();
}
}
}
super.route(wrappedElement);
if (isUnsolicitedAckExpected()) {
session.getStreamManager().sendServerAcknowledgement();
}
}
}
private boolean isUnsolicitedAckExpected() {
if (!session.getStreamManager().isEnabled()) {
return false;
}
return unsolicitedAckFrequency > 0 && session.getNumClientPackets() % unsolicitedAckFrequency == 0;
}
private boolean isUnsolicitedAckExpected() {
if (!session.getStreamManager().isEnabled()) {
return false;
}
return unsolicitedAckFrequency > 0 && session.getNumClientPackets() % unsolicitedAckFrequency == 0;
}
}
......@@ -63,342 +63,342 @@ import javax.xml.XMLConstants;
@WebSocket
public class XmppWebSocket {
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 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 static Logger Log = LoggerFactory.getLogger( XmppWebSocket.class );
private static GenericObjectPool<XMPPPacketReader> readerPool;
private static Logger Log = LoggerFactory.getLogger( XmppWebSocket.class );
private static GenericObjectPool<XMPPPacketReader> readerPool;
private SessionPacketRouter router;
private Session wsSession;
private WebSocketConnection wsConnection;
private LocalClientSession xmppSession;
private Session wsSession;
private WebSocketConnection wsConnection;
private LocalClientSession xmppSession;
private boolean startedSASL = false;
private Status saslStatus;
private TimerTask pingTask;
public XmppWebSocket() {
if (readerPool == null) {
initializePool();
}
}
// WebSocket event handlers
@OnWebSocketConnect
public void onConnect(Session session)
{
wsSession = session;
wsConnection = new WebSocketConnection(this, session.getRemoteAddress());
pingTask = new PingTask();
TaskEngine.getInstance().schedule(pingTask, JiveConstants.MINUTE, JiveConstants.MINUTE);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
closeSession();
}
@OnWebSocketMessage
public void onTextMethod(String 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 XMPP stanza", ex);
} finally {
if (reader != null) {
readerPool.returnObject(reader);
}
}
}
@OnWebSocketError
public void onError(Throwable error)
{
Log.error("Error detected; session: " + wsSession, error);
closeStream(new StreamError(StreamError.Condition.internal_server_error));
try {
if (wsSession != null) {
wsSession.disconnect();
}
} catch ( Exception e ) {
Log.error("Error disconnecting websocket", e);
}
}
// local (package) visibility
boolean isWebSocketOpen() {
return wsSession != null && wsSession.isOpen();
}
boolean isWebSocketSecure() {
return wsSession != null && wsSession.isSecure();
}
void closeWebSocket()
public XmppWebSocket() {
if (readerPool == null) {
initializePool();
}
}
// WebSocket event handlers
@OnWebSocketConnect
public void onConnect(Session session)
{
wsSession = session;
wsConnection = new WebSocketConnection(this, session.getRemoteAddress());
pingTask = new PingTask();
TaskEngine.getInstance().schedule(pingTask, JiveConstants.MINUTE, JiveConstants.MINUTE);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
closeSession();
}
@OnWebSocketMessage
public void onTextMethod(String stanza)
{
if (isWebSocketOpen())
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 XMPP stanza", ex);
} finally {
if (reader != null) {
readerPool.returnObject(reader);
}
}
}
@OnWebSocketError
public void onError(Throwable error)
{
Log.error("Error detected; session: " + wsSession, error);
closeStream(new StreamError(StreamError.Condition.internal_server_error));
try {
if (wsSession != null) {
wsSession.disconnect();
}
} catch ( Exception e ) {
Log.error("Error disconnecting websocket", e);
}
}
// local (package) visibility
boolean isWebSocketOpen() {
return wsSession != null && wsSession.isOpen();
}
boolean isWebSocketSecure() {
return wsSession != null && wsSession.isSecure();
}
void closeWebSocket()
{
if (isWebSocketOpen())
{
wsSession.close();
}
wsSession = null;
wsSession = null;
}
void closeSession() {
if (isWebSocketOpen()) {
closeStream(null);
}
if (xmppSession != null) {
xmppSession.close();
SessionManager.getInstance().removeSession(xmppSession);
xmppSession = null;
}
}
void closeSession() {
if (isWebSocketOpen()) {
closeStream(null);
}
if (xmppSession != null) {
xmppSession.close();
SessionManager.getInstance().removeSession(xmppSession);
xmppSession = null;
}
}
/**
* Send an XML packet to the remote peer
*
* @param packet XML to be sent to client
*/
void deliver(String packet)
/**
* Send an XML packet to the remote peer
*
* @param packet XML to be sent to client
*/
void deliver(String packet)
{
if (isWebSocketOpen())
{
try {
xmppSession.incrementServerPacketCount();
wsSession.getRemote().sendStringByFuture(packet);
xmppSession.incrementServerPacketCount();
wsSession.getRemote().sendStringByFuture(packet);
} catch (Exception e) {
Log.error("Packet delivery failed; session: " + wsSession, e);
Log.warn("Failed to deliver packet:\n" + packet );
Log.warn("Failed to deliver packet:\n" + packet );
}
} else {
Log.warn("Failed to deliver packet; socket is closed:\n" + packet);
Log.warn("Failed to deliver packet; socket is closed:\n" + packet);
}
}
static boolean isCompressionEnabled() {
return JiveGlobals.getProperty(
ConnectionSettings.Client.COMPRESSION_SETTINGS, Connection.CompressionPolicy.optional.toString())
.equalsIgnoreCase(Connection.CompressionPolicy.optional.toString());
}
// helper/utility methods
/*
* 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) {
static boolean isCompressionEnabled() {
return JiveGlobals.getProperty(
ConnectionSettings.Client.COMPRESSION_SETTINGS, Connection.CompressionPolicy.optional.toString())
.equalsIgnoreCase(Connection.CompressionPolicy.optional.toString());
}
// helper/utility methods
/*
* 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"));
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())) {
// 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)) {
} 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(wsConnection, language);
xmppSession.setSessionData("ws", Boolean.TRUE);
// valid stream; initiate session
xmppSession = SessionManager.getInstance().createClientSession(wsConnection, language);
xmppSession.setSessionData("ws", Boolean.TRUE);
}
if (xmppSession == null) {
closeStream(streamError);
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);
}
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() {
/*
* 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'/>");
}
// 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));
sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V3));
}
}
sb.append("</stream:features>");
deliver(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'/>");
}
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'/>");
deliver(sb.toString());
}
}
private void closeStream(StreamError streamError)
private void closeStream(StreamError streamError)
{
if (isWebSocketOpen()) {
if (streamError != null) {
deliver(streamError.toXML());
}
StringBuilder sb = new StringBuilder(250);
sb.append("<close ");
sb.append("xmlns='").append(FRAMING_NAMESPACE).append("'");
sb.append("/>");
deliver(sb.toString());
closeWebSocket();
}
if (isWebSocketOpen()) {
if (streamError != null) {
deliver(streamError.toXML());
}
StringBuilder sb = new StringBuilder(250);
sb.append("<close ");
sb.append("xmlns='").append(FRAMING_NAMESPACE).append("'");
sb.append("/>");
deliver(sb.toString());
closeWebSocket();
}
}
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());
deliver(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);
}
//-- Keep-alive ping for idle peers
private final class PingTask extends TimerTask {
private boolean lastPingFailed = false;
@Override
public void run() {
if (!isWebSocketOpen()) {
TaskEngine.getInstance().cancelScheduledTask(pingTask);
} else {
long idleTime = System.currentTimeMillis() - JiveConstants.MINUTE;
if (xmppSession.getLastActiveDate().getTime() >= idleTime) {
return;
}
try {
// see https://tools.ietf.org/html/rfc6455#section-5.5.2
wsSession.getRemote().sendPing(null);
lastPingFailed = false;
} catch (IOException ioe) {
Log.error("Failed to ping remote peer: " + wsSession, ioe);
if (lastPingFailed) {
closeSession();
TaskEngine.getInstance().cancelScheduledTask(pingTask);
} else {
lastPingFailed = true;
}
}
}
}
}
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());
deliver(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);
}
//-- Keep-alive ping for idle peers
private final class PingTask extends TimerTask {
private boolean lastPingFailed = false;
@Override
public void run() {
if (!isWebSocketOpen()) {
TaskEngine.getInstance().cancelScheduledTask(pingTask);
} else {
long idleTime = System.currentTimeMillis() - JiveConstants.MINUTE;
if (xmppSession.getLastActiveDate().getTime() >= idleTime) {
return;
}
try {
// see https://tools.ietf.org/html/rfc6455#section-5.5.2
wsSession.getRemote().sendPing(null);
lastPingFailed = false;
} catch (IOException ioe) {
Log.error("Failed to ping remote peer: " + wsSession, ioe);
if (lastPingFailed) {
closeSession();
TaskEngine.getInstance().cancelScheduledTask(pingTask);
} else {
lastPingFailed = true;
}
}
}
}
}
}
\ No newline at end of file
......@@ -50,7 +50,6 @@
<module>userCreation</module>
<module>userImportExport</module>
<module>userservice</module>
<module>websocket</module>
<module>xmldebugger</module>
</modules>
......
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Openfire WebSocket Plugin Changelog</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
}
H2 {
font-size : 10pt;
font-weight : bold;
padding-left : 1em;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
</style>
</head>
<body>
<h1>
Openfire WebSocket Plugin Changelog
</h1>
<p><b>1.2.1</b> -- August 7, 2017</p>
<ul>
<li>[<a href='https://igniterealtime.org/issues/browse/OF-1362'></a>] - OF-1362: Websocket plugin has been broken since Openfire 4.1.4.</li>
<li>[<a href='https://igniterealtime.org/issues/browse/OF-1353'></a>] - OF-1353: Define a 'priorToServerVersion' in preparation of <a href="https://issues.igniterealtime.org/browse/OF-1339">OF-1339: </a> Merge websocket plugin with Openfire core.</li>
<li>Minimum server requirement: 4.1.5</li>
<li>Prior-to server requirement: 4.2.0</li>
</ul>
<p><b>1.2.0</b> -- May 8, 2017</p>
<ul>
<li>[<a href='https://igniterealtime.org/issues/browse/OF-1326'></a>] - Made compatible with new HttpBind API in Openfire 4.2.0</li>
<li>Minimum server requirement: 4.2.0 Alpha</li>
</ul>
<p><b>1.1.4</b> -- March 3, 2016</p>
<ul>
<li>[<a href='https://igniterealtime.org/issues/browse/OF-1097'></a>] - Non-SASL Authentication support should be optional.</li>
<li>Minimum server requirement: 4.1.0 Alpha</li>
</ul>
<p><b>1.1.3</b> -- January 6, 2016</p>
<ul>
<li>Update dependencies and documentation for pending Openfire 4.0 release.</li>
</ul>
<p><b>1.1.2</b> -- December 17, 2015</p>
<ul>
<li>OF-1006: Avoid connection blocking with improved handling for pooled resources.</li>
</ul>
<p><b>1.1</b> -- November 24, 2015</p>
<ul>
<li>Pass 'xml:lang' attribute to the session.</li>
</ul>
<p><b>1.0.1</b> -- November 21, 2015</p>
<ul>
<li>"lang" attribute should be "xml:lang" in session creation response.</li>
</ul>
<p><b>1.0</b> -- July 28, 2015</p>
<ul>
<li>Initial release. </li>
</ul>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Plugin configuration for the WebSocket plugin.
-->
<plugin>
<class>org.jivesoftware.openfire.websocket.WebSocketPlugin</class>
<name>Openfire WebSocket</name>
<description>Provides WebSocket support for Openfire.</description>
<author>Tom Evans</author>
<version>1.2.1</version>
<date>08/07/2017</date>
<url>https://tools.ietf.org/html/rfc7395</url>
<minServerVersion>4.1.5</minServerVersion>
<priorToServerVersion>4.2.0</priorToServerVersion>
</plugin>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>plugins</artifactId>
<groupId>org.igniterealtime.openfire</groupId>
<version>4.2.0-SNAPSHOT</version>
</parent>
<groupId>org.igniterealtime.openfire.plugins</groupId>
<artifactId>websocket</artifactId>
<version>1.2.1</version>
<name>WebSocket Plugin</name>
<description>Provides WebSocket support for Openfire.</description>
<developers>
<developer>
<name>Tom Evans</name>
</developer>
</developers>
<build>
<sourceDirectory>src/java</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
</project>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Openfire WebSocket Plugin Readme</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
}
H2 {
font-size : 10pt;
font-weight : bold;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
</style>
</head>
<body>
<h1>
Openfire WebSocket Plugin Readme
</h1>
<h2>Overview</h2>
<p>
This plugin extends Openfire to support WebSocket. The implementation follows the XMPP WebSocket subprotocol
(<a href="https://tools.ietf.org/html/rfc7395">RFC 7395</a>) specification, which is a standard extension of the
WebSocket protocol specification (<a href="https://tools.ietf.org/html/rfc6455">RFC 6455</a>).
</p>
<p>
Note that the BOSH (http-bind) capabilities of Openfire must be enabled and correctly configured as a
prerequisite before installing this plugin. The WebSocket servlet is installed within the same context
as the BOSH component, and will reuse the same HTTP/S port(s) when establishing the WebSocket connection.
</p>
<h2>Installation</h2>
<p>Please note that the functionality provided by this plugin is <a href="https://issues.igniterealtime.org/browse/OF-1339">
added to Openfire direclty in version 4.2.0</a>. <em>This plugin is no longer needed in Openfire 4.2.0 or later!</em>
</p>
<p>Copy websocket.jar into the plugins directory of your Openfire installation. The
plugin will then be automatically deployed. To upgrade to a new version, copy the new
websocket.jar file over the existing file.</p>
<p>
Upon installation, the WebSocket URI path will be /ws/ on the same server/port as the BOSH
connector. To establish a secure WebSocket, modify the following URL as appropriate:
</p>
<pre>
wss://your.openfire.host:7443/ws/
</pre>
<h2>Configuration</h2>
<p>
The WebSocket plugin implements the Stream Management (<a href="http://xmpp.org/extensions/xep-0198.html">XEP-0198</a>)
"ack" capabilities introduced with Openfire 4.0. This provides assurance for XMPP packet delivery by allowing the peers
to agree on the number of stanzas exchanged. Two system properties are available to configure this feature:
</p>
<dl>
<dt><pre>stream.management.active</pre></dt>
<dd>Boolean property to enable/disable stream management (default: true)</dd>
<dt><pre>stream.management.unsolicitedAckFrequency</pre></dt>
<dd>Integer property indicating frequency of unsolicited ack's from the server to the client (default: 0)</dd>
</dl>
<p>
XEP-0198 allows either party (client or server) to send unsolicited ack/answer
stanzas on a periodic basis. This implementation approximates BOSH ack behavior
by sending unsolicited <a /> stanzas from the server to the client after a
configurable number of stanzas have been received from the client.
</p>
<p>
Setting the system property to "1" would indicate that each client packet should
be ack'd by the server when stream management is enabled for a particular stream.
To disable unsolicited server acks, use the default value for system property
"stream.management.unsolicitedAckFrequency" ("0"). This setting does not affect
server responses to explicit ack requests from the client.
</p>
</body>
</html>
......@@ -279,6 +279,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment