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;
}
}
......@@ -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