Unverified Commit 04caa965 authored by Dave Cridland's avatar Dave Cridland Committed by GitHub

Merge pull request #1001 from guusdk/XEP_0215_External_Service_Discovery_plugin

OF-1469 New plugin: XEP-0215 External Service Discovery
parents 7f3cd43d 321c282d
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>External Service Discovery 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>
External Service Discovery Plugin Changelog
</h1>
<p><b>1.0.0</b> -- January 26, 2018</p>
<ul>
<li>Initial release.</li>
</ul>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<class>org.igniterealtime.openfire.plugins.externalservicediscovery.ExternalServiceDiscoveryPlugin</class>
<name>External Service Discovery</name>
<description>Allows XMPP entities to discover services external to the XMPP network, such as STUN and TURN servers.</description>
<author>Guus der Kinderen</author>
<version>1.0.0</version>
<date>01/26/2018</date>
<databaseKey>externalservicediscovery</databaseKey>
<databaseVersion>1</databaseVersion>
<adminconsole>
<tab id="sidebar-media-services">
<item id="external-service-discovery-config" name="${sidebar.external-service-discovery}"
url="external-service-discovery-config.jsp"
description="${sidebar.external-service-discovery.descr}"/>
</tab>
</adminconsole>
</plugin>
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>plugins</artifactId>
<groupId>org.igniterealtime.openfire</groupId>
<version>4.3.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.igniterealtime.openfire.plugins</groupId>
<artifactId>externalservicediscovery</artifactId>
<version>1.0.0</version>
<name>External Service Discovery Plugin</name>
<description>Openfire plugin that implements XEP-0215: External Service Discovery, allowing XMPP entities to discover services external to the XMPP network.</description>
<build>
<sourceDirectory>src/java</sourceDirectory>
<testSourceDirectory>src/test</testSourceDirectory>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<!-- Compiles the Openfire Admin Console JSP pages. -->
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jspc-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Versions of Openfire up to 4.2.1 included an older slf4j-api artifact (1.6.6). Fixating the dependency. -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>2.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>External Service Discovery 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;
}
H3 {
font-size : 10pt;
font-style: italic;
color: #004444;
}
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%;
}
#datatable TH {
color : #fff;
background-color : #2A448C;
text-align : left;
}
#datatable TD {
background-color : #FAF6EF;
}
#datatable .name {
background-color : #DCE2F5;
}
</style>
</head>
<body>
<h1>
External Service Discovery Plugin Readme
</h1>
<h2>Overview</h2>
<p>
The External Service Discovery plugin allows XMPP entities to discover services that are external to the XMPP
network. It is typically used to allow clients to interact with STUN and TURN services, possibly making use of
temporary credentials for those servers. This plugin provides an implementation of
<a href="https://xmpp.org/extensions/xep-0215.html">XEP-0215: External Service Discovery</a>.
</p>
<h2>Installation</h2>
<p>
Copy externalservicediscovery.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 externalservicediscovery.jar file over the
existing file.
</p>
<h2>Configuration</h2>
<p>
The plugin is configured via the Openfire Admin Console. After installation, a new Admin Console page is available.
The page can be found on a main menu in the "Server" tab, "Media Services" sub-tab. The name of the page is
"Ext. Service Discovery".
</p>
<h2>Attribution</h2>
<p>
Icons made by <a href="http://www.freepik.com" title="Freepik">Freepik</a> from
<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by
<a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a>
</p>
</body>
</html>
CREATE TABLE ofExternalServices (
serviceID INT NOT NULL,
name VARCHAR(255),
host VARCHAR(255) NOT NULL,
port INT,
restricted BOOLEAN,
transport CHAR(3),
type VARCHAR(10) NOT NULL,
username VARCHAR(255),
password VARCHAR(1024),
sharedSecret VARCHAR(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
CREATE TABLE ofExternalServices (
serviceID BIGINT NOT NULL,
name VARCHAR(255),
host VARCHAR(255) NOT NULL,
port INT,
restricted BOOLEAN,
transport CHAR(3),
type VARCHAR(10) NOT NULL,
username VARCHAR(255),
password VARCHAR(1024),
sharedSecret VARCHAR(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
CREATE TABLE ofExternalServices (
serviceID BIGINT NOT NULL,
name VARCHAR(255),
host VARCHAR(255) NOT NULL,
port INT,
restricted BOOLEAN,
transport CHAR(3),
type VARCHAR(10) NOT NULL,
username VARCHAR(255),
password VARCHAR(1024),
sharedSecret VARCHAR(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
CREATE TABLE ofExternalServices (
serviceID INTEGER NOT NULL,
name VARCHAR2(255),
host VARCHAR2(255) NOT NULL,
port INT,
restricted SMALLINT,
transport CHAR(3),
type VARCHAR2(10) NOT NULL,
username VARCHAR2(255),
password VARCHAR2(1024),
sharedSecret VARCHAR2(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
CREATE TABLE ofExternalServices (
serviceID BIGINT NOT NULL,
name VARCHAR(255),
host VARCHAR(255) NOT NULL,
port INT,
restricted BOOLEAN,
transport CHAR(3),
type VARCHAR(10) NOT NULL,
username VARCHAR(255),
password VARCHAR(1024),
sharedSecret VARCHAR(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
CREATE TABLE ofExternalServices (
serviceID INTEGER NOT NULL,
name NVARCHAR(255),
host NVARCHAR(255) NOT NULL,
port INT,
restricted BIT,
transport NCHAR(3),
type NVARCHAR(10) NOT NULL,
username NVARCHAR(255),
password NVARCHAR(1024),
sharedSecret NVARCHAR(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
CREATE TABLE ofExternalServices (
serviceID INTEGER NOT NULL,
name NVARCHAR(255),
host NVARCHAR(255) NOT NULL,
port INT,
restricted BIT,
transport NCHAR(3),
type NVARCHAR(10) NOT NULL,
username NVARCHAR(255),
password NVARCHAR(1024),
sharedSecret NVARCHAR(1024)
);
INSERT INTO ofID (idType, id) VALUES (937, 1);
INSERT INTO ofVersion (name, version) VALUES ('externalservicediscovery', 1);
global.csrf.failed=CSRF Error: No changes made, you'll need to retry.
global.click_delete=Delete
admin.error=Internal server error
sidebar.external-service-discovery=Ext. Service Discovery
sidebar.external-service-discovery.descr=Settings that allow users to detect and use STUN and TURN services that are offered by external servers.
external-service-discovery-config.success-delete=Service removed.
external-service-discovery-config.success-add=Service added.
external-service-discovery-config.title=External Service Discovery Configuration
external-service-discovery-config.descr=On this page, you can configure external services (like STUN and TURN servers) that can be used by your users.
external-service-discovery-config.host=Host
external-service-discovery-config.host-required=A 'host' value is required.
external-service-discovery-config.port=Port
external-service-discovery-config.port-number=The 'port' value must be numeric.
external-service-discovery-config.name=Description
external-service-discovery-config.transport=Transport
external-service-discovery-config.tcp=TCP
external-service-discovery-config.udp=UDP
external-service-discovery-config.type=Type
external-service-discovery-config.type-required=A 'type' value is required.
external-service-discovery-config.stun=STUN
external-service-discovery-config.turn=TURN
external-service-discovery-config.turns=TURNS
external-service-discovery-config.credentials=Credentials
external-service-discovery-config.table.empty=There currently are no external services configured.
external-service-discovery-config.add-service.title=Add External Service
external-service-discovery-config.add-service.descr=You can add an external service by filling out the form below.
external-service-discovery-config.add-service.button-caption=Add Service
external-service-discovery-config.credentials.nocredentials=No credentials needed
external-service-discovery-config.credentials.using-nocredentials=Not using any credentials (implies unauthenticated access)
external-service-discovery-config.credentials.userpass=Hardcoded
external-service-discovery-config.credentials.using-userpass=Using hardcoded credentials with username:
external-service-discovery-config.credentials.username=Username
external-service-discovery-config.credentials.username-required=When using hardcoded credentials, a 'username' value is required.
external-service-discovery-config.credentials.password=Password
external-service-discovery-config.credentials.password-required=When using hardcoded credentials, a 'password' value is required.
external-service-discovery-config.credentials.sharedsecret=Shared Secret (for generating ephemeral passwords)
external-service-discovery-config.credentials.sharedsecret-required=When generating ephemeral passwords, a 'shared secret' value is required.
external-service-discovery-config.credentials.using-sharedsecret=Using shared secret to generate ephemeral passwords.
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import java.util.Date;
/**
* Representation of service credentials.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class Credentials
{
/**
* A timestamp indicating when the provided username and password credentials will expire. The format MUST adhere
* to the dateTime format specified in XMPP Date and Time Profiles (XEP-0082) [12] and MUST be expressed in UTC.
*
* Optional.
*/
private final Date expires;
/**
* A service- or server-generated password for use at the service.
*
* Optional.
*/
private final String password;
/**
* A service- or server-generated username for use at the service.
*
* Optional.
*/
private final String username;
public Credentials( String username, String password, Date expires )
{
this.username = username;
this.password = password;
this.expires = expires;
}
public Date getExpires()
{
return expires;
}
public String getPassword()
{
return password;
}
public String getUsername()
{
return username;
}
@Override
public boolean equals( Object o )
{
if ( this == o )
{
return true;
}
if ( o == null || getClass() != o.getClass() )
{
return false;
}
final Credentials that = (Credentials) o;
if ( expires != null ? !expires.equals( that.expires ) : that.expires != null )
{
return false;
}
if ( password != null ? !password.equals( that.password ) : that.password != null )
{
return false;
}
return username != null ? username.equals( that.username ) : that.username == null;
}
@Override
public int hashCode()
{
int result = expires != null ? expires.hashCode() : 0;
result = 31 * result + ( password != null ? password.hashCode() : 0 );
result = 31 * result + ( username != null ? username.hashCode() : 0 );
return result;
}
}
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import org.dom4j.Element;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.handler.IQHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
/**
* An IQ handler that implements XEP-0215: External Service Discovery.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
* @see <a href="https://xmpp.org/extensions/xep-0215.html">XEP-0215: External Service Discovery</a>
*/
public abstract class ExternalServiceDiscoveryIQHandler extends IQHandler implements ServerFeaturesProvider
{
private static final Logger Log = LoggerFactory.getLogger( ExternalServiceDiscoveryIQHandler.class );
public ExternalServiceDiscoveryIQHandler()
{
super( "XEP-0215: External Service Discovery" );
}
@Override
public Iterator<String> getFeatures()
{
return Collections.singleton( getInfo().getNamespace() ).iterator();
}
@Override
public IQ handleIQ( IQ packet ) throws UnauthorizedException
{
if ( packet.isResponse() )
{
Log.debug( "Silently ignoring IQ response: {}", packet );
return null;
}
if ( IQ.Type.set == packet.getType() )
{
Log.info( "Responding with an error to an IQ request of type 'set': {}", packet );
final IQ response = IQ.createResultIQ( packet );
response.setError( PacketError.Condition.service_unavailable );
return response;
}
final IQ response;
switch ( packet.getChildElement().getName() )
{
case "services":
response = handleServices( packet );
break;
case "credentials":
response = handleCredentials( packet );
break;
default:
Log.info( "Responding with an error to an IQ request for which the element name escaped by namespace is not understood: {}", packet );
response = IQ.createResultIQ( packet );
response.setError( PacketError.Condition.service_unavailable );
}
Log.info( "Responding with {} to request {}", response.toXML(), packet.toXML() );
return response;
}
/**
* Handles requests for services.
*
* @param request the request (cannot be null).
* @return An answer (never null).
*/
protected IQ handleServices( IQ request )
{
final Map<Service, Credentials> services;
final ServiceManager serviceManager = ServiceManager.getInstance();
final String requestedType = request.getChildElement().attributeValue( "type" );
if ( requestedType == null || requestedType.isEmpty() )
{
Log.debug( "Handling request for all services." );
services = serviceManager.getServicesFor( request.getFrom() );
}
else
{
Log.debug( "Handling request for services of a specific type: {}.", requestedType );
services = serviceManager.getServicesFor( request.getFrom(), requestedType );
}
// Formulate response.
final IQ response = IQ.createResultIQ( request );
final Element childElement = response.setChildElement( request.getChildElement().getName(), request.getChildElement().getNamespaceURI() );
if ( requestedType != null && !requestedType.isEmpty() )
{
childElement.addAttribute( "type", requestedType );
}
for ( final Map.Entry<Service, Credentials> entry : services.entrySet() )
{
addServiceXml( childElement, entry.getKey(), null, entry.getValue() );
}
return response;
}
/**
* Handles requests for credentials
*
* @param request the request (cannot be null).
* @return An answer (never null).
*/
protected IQ handleCredentials( IQ request )
{
final Element requestedService = request.getChildElement().element( "service" );
if ( requestedService == null )
{
Log.debug( "Responding with an error to a request for credentials that did not specify any service: {}", request );
final IQ response = IQ.createResultIQ( request );
response.setError( PacketError.Condition.bad_request );
return response;
}
final String requestedHost = requestedService.attributeValue( "host" );
final String requestedType = requestedService.attributeValue( "type" );
final Integer requestedPort;
if ( requestedService.attribute( "port" ) != null )
{
try
{
requestedPort = Integer.parseInt( requestedService.attributeValue( "port" ) );
}
catch ( NumberFormatException ex )
{
Log.debug( "Responding with an error to a request for credentials that specified a malformed port number for a service: {}", request, ex );
final IQ response = IQ.createResultIQ( request );
response.setError( PacketError.Condition.bad_request );
return response;
}
}
else
{
requestedPort = null;
}
if ( requestedHost == null || requestedHost.isEmpty() || requestedType == null || requestedType.isEmpty() )
{
Log.debug( "Responding with an error to a request for credentials that did not specify any service: {}", request );
final IQ response = IQ.createResultIQ( request );
response.setError( PacketError.Condition.bad_request );
return response;
}
final ServiceManager serviceManager = ServiceManager.getInstance();
final Map<Service, Credentials> services;
if ( requestedPort == null )
{
Log.debug( "Handling request for credentials by {} for the {} service: {}", request.getFrom(), requestedType, requestedHost );
services = serviceManager.getServicesFor( request.getFrom(), requestedHost, requestedType );
}
else
{
Log.debug( "Handling request for credentials by {} for the {} service: {}:{}", request.getFrom(), requestedType, requestedHost, requestedPort);
services = serviceManager.getServicesFor( request.getFrom(), requestedHost, requestedType, requestedPort );
}
// Formulate response.
final IQ response = IQ.createResultIQ( request );
final Element childElement = response.setChildElement( "credentials", request.getChildElement().getNamespaceURI() );
for ( final Map.Entry<Service, Credentials> service : services.entrySet() )
{
addServiceXml( childElement, service.getKey(), null, service.getValue() );
}
return response;
}
abstract void addServiceXml( Element parent, Service service, final Service.Action action, final Credentials credentials );
}
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import org.dom4j.Element;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
/**
* An IQ handler that implements XEP-0215: External Service Discovery, Version 0.6 (2014-02-27)
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
* @see <a href="https://xmpp.org/extensions/xep-0215.html">XEP-0215: External Service Discovery</a>
*/
public class ExternalServiceDiscoveryIQHandlerV1 extends ExternalServiceDiscoveryIQHandler
{
private static final Logger Log = LoggerFactory.getLogger( ExternalServiceDiscoveryIQHandlerV1.class );
private final static IQHandlerInfo INFO = new IQHandlerInfo( "services", "urn:xmpp:extdisco:1" );
@Override
public IQHandlerInfo getInfo()
{
return INFO;
}
@Override
public void addServiceXml( Element parent, Service service, final Service.Action action, final Credentials credentials )
{
final Element result = parent.addElement( "service" );
if ( credentials != null )
{
if ( credentials.getUsername() != null )
{
result.addAttribute( "username", credentials.getUsername() );
}
if ( credentials.getPassword() != null )
{
result.addAttribute( "password", credentials.getPassword() );
}
}
if ( service.getHost() != null )
{
result.addAttribute( "host", service.getHost() );
}
if ( service.getName() != null )
{
result.addAttribute( "name", service.getName() );
}
if ( service.getPort() != null )
{
result.addAttribute( "port", Integer.toString( service.getPort() ) );
}
if ( service.getTransport() != null )
{
result.addAttribute( "transport", service.getTransport() );
}
if ( service.getType() != null )
{
result.addAttribute( "type", service.getType() );
}
}
}
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import org.dom4j.Element;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.util.XMPPDateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
/**
* An IQ handler that implements XEP-0215: External Service Discovery, Version 0.7 (2015-10-20).
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
* @see <a href="https://xmpp.org/extensions/xep-0215.html">XEP-0215: External Service Discovery</a>
*/
public class ExternalServiceDiscoveryIQHandlerV2 extends ExternalServiceDiscoveryIQHandler
{
private static final Logger Log = LoggerFactory.getLogger( ExternalServiceDiscoveryIQHandlerV2.class );
private final static IQHandlerInfo INFO = new IQHandlerInfo( "services", "urn:xmpp:extdisco:2" );
@Override
public IQHandlerInfo getInfo()
{
return INFO;
}
@Override
public void addServiceXml( Element parent, Service service, final Service.Action action, final Credentials credentials )
{
final Element result = parent.addElement( "service" );
if ( action != null )
{
result.addAttribute( "action", action.toString() );
}
if ( credentials != null )
{
if ( credentials.getExpires() != null )
{
result.addAttribute( "expires", XMPPDateTimeFormat.format( credentials.getExpires() ) );
}
if ( credentials.getUsername() != null )
{
result.addAttribute( "username", credentials.getUsername() );
}
if ( credentials.getPassword() != null )
{
result.addAttribute( "password", credentials.getPassword() );
}
}
if ( service.getHost() != null )
{
result.addAttribute( "host", service.getHost() );
}
if ( service.getName() != null )
{
result.addAttribute( "name", service.getName() );
}
if ( service.getPort() != null )
{
result.addAttribute( "port", Integer.toString( service.getPort() ) );
}
if ( service.getRestricted() != null )
{
result.addAttribute( "restricted", Boolean.toString( service.getRestricted() ) );
}
if ( service.getTransport() != null )
{
result.addAttribute( "transport", service.getTransport() );
}
if ( service.getType() != null )
{
result.addAttribute( "type", service.getType() );
}
}
}
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.Iterator;
/**
* An Openfire plugin that implements XEP-0215: External Service Discovery.
*
* This plugin allowS XMPP entities to discover services external to the XMPP network, such as STUN and TURN servers.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
* @see <a href="https://xmpp.org/extensions/xep-0215.html">XEP-0215: External Service Discovery</a>
*/
public class ExternalServiceDiscoveryPlugin implements Plugin
{
private static final Logger Log = LoggerFactory.getLogger( ExternalServiceDiscoveryPlugin.class );
private ExternalServiceDiscoveryIQHandlerV1 iqHandlerV1;
private ExternalServiceDiscoveryIQHandlerV2 iqHandlerV2;
@Override
public void initializePlugin( PluginManager manager, File pluginDirectory )
{
Log.debug( "Registering IQ Handlers..." );
iqHandlerV1 = new ExternalServiceDiscoveryIQHandlerV1();
iqHandlerV2 = new ExternalServiceDiscoveryIQHandlerV2();
XMPPServer.getInstance().getIQRouter().addHandler( iqHandlerV1 );
XMPPServer.getInstance().getIQRouter().addHandler( iqHandlerV2 );
Log.debug( "Registering Server Features..." );
for ( final Iterator<String> it = iqHandlerV1.getFeatures(); it.hasNext(); )
{
XMPPServer.getInstance().getIQDiscoInfoHandler().addServerFeature( it.next() );
}
for ( final Iterator<String> it = iqHandlerV2.getFeatures(); it.hasNext(); )
{
XMPPServer.getInstance().getIQDiscoInfoHandler().addServerFeature( it.next() );
}
Log.debug( "Initialized." );
}
@Override
public void destroyPlugin()
{
Log.debug( "Removing Server Features..." );
for ( final Iterator<String> it = iqHandlerV2.getFeatures(); it.hasNext(); )
{
XMPPServer.getInstance().getIQDiscoInfoHandler().removeServerFeature( it.next() );
}
for ( final Iterator<String> it = iqHandlerV1.getFeatures(); it.hasNext(); )
{
XMPPServer.getInstance().getIQDiscoInfoHandler().removeServerFeature( it.next() );
}
if ( iqHandlerV2 != null )
{
Log.debug( "Removing IQ Handler..." );
XMPPServer.getInstance().getIQRouter().removeHandler( iqHandlerV2 );
}
if ( iqHandlerV1 != null )
{
Log.debug( "Removing IQ Handler..." );
XMPPServer.getInstance().getIQRouter().removeHandler( iqHandlerV1 );
}
Log.debug( "Destroyed." );
}
}
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import org.jivesoftware.database.JiveID;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
/**
* A representation of a Service object.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
@JiveID( 937 )
public final class Service
{
private final static Logger Log = LoggerFactory.getLogger( Service.class );
private final long databaseId;
/**
* When sending a push update, the action value indicates if the service is being added or deleted from the set of
* known services (or simply being modified). The defined values are "add", "remove", and "modify", where "add" is
* the default.
*/
enum Action {
add,
remove,
modify
}
/**
* Either a fully qualified domain name (FQDN) or an IP address (IPv4 or IPv6).
*
* Required.
*/
private final String host;
/**
* A friendly (human-readable) name or label for the service.
*
* Optional.
*/
private final String name;
/**
* The communications port to be used at the host.
*
* Recommended.
*/
private final Integer port;
/**
* A boolean value indicating that username and password credentials are required and will need to be requested if
* not already provided (see Requesting Credentials).
*
* Optional.
*/
private final Boolean restricted;
/**
* The underlying transport protocol to be used when communicating with the service (typically either TCP or UDP).
*
* Recommended.
*/
private final String transport;
/**
* The service type as registered with the XMPP Registrar.
*
* Required.
*/
private final String type;
/**
* The (hard-coded) credentials to use in this service.
*
* Optional.
*/
private final Credentials credentials;
/**
* A secret shared with the TURN server, used to generate ephemeral credentials.
*
* Optional.
*/
private final SecretKeySpec secretKey;
public Service( String name, String host, Integer port, String transport, String type )
{
if ( host == null || host.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'host' cannot be null or an empty string." );
}
if ( type == null || type.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'type' cannot be null or an empty string." );
}
this.databaseId = SequenceManager.nextID( this );
this.host = host;
this.name = name;
this.port = port;
this.restricted = false;
this.transport = transport;
this.type = type;
this.credentials = null;
this.secretKey = null;
}
public Service( String name, String host, Integer port, String transport, String type, String username, String password )
{
if ( host == null || host.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'host' cannot be null or an empty string." );
}
if ( type == null || type.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'type' cannot be null or an empty string." );
}
this.databaseId = SequenceManager.nextID( this );
this.host = host;
this.name = name;
this.port = port;
this.restricted = true; // technically, could be false, but that'll probably be confusing more than a feature.
this.transport = transport;
this.type = type;
this.credentials = new Credentials( username, password, null );
this.secretKey = null;
}
public Service( String name, String host, Integer port, String transport, String type, String sharedSecret )
{
if ( host == null || host.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'host' cannot be null or an empty string." );
}
if ( type == null || type.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'type' cannot be null or an empty string." );
}
this.databaseId = SequenceManager.nextID( this );
this.host = host;
this.name = name;
this.port = port;
this.restricted = true; // technically, could be false, but that'll probably be confusing more than a feature.
this.transport = transport;
this.type = type;
this.credentials = null;
if ( sharedSecret == null || sharedSecret.isEmpty() )
{
this.secretKey = null;
}
else
{
this.secretKey = new SecretKeySpec( sharedSecret.getBytes( StandardCharsets.UTF_8 ), "HmacSHA1" );
}
}
Service( long databaseId, String name, String host, Integer port, Boolean restricted, String transport, String type, String username, String password, String sharedSecret )
{
if ( host == null || host.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'host' cannot be null or an empty string." );
}
if ( type == null || type.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'type' cannot be null or an empty string." );
}
this.databaseId = databaseId;
this.host = host;
this.name = name;
this.port = port;
this.restricted = restricted;
this.transport = transport;
this.type = type;
if ( username == null && password == null )
{
this.credentials = null;
}
else
{
this.credentials = new Credentials( username, password, null );
}
if ( sharedSecret == null )
{
this.secretKey = null;
}
else
{
this.secretKey = new SecretKeySpec( sharedSecret.getBytes( StandardCharsets.UTF_8 ), "HmacSHA1" );
}
}
public long getDatabaseId() { return databaseId; }
public String getHost()
{
return host;
}
public String getName()
{
return name;
}
public Integer getPort()
{
return port;
}
public Boolean getRestricted()
{
return restricted;
}
public String getTransport()
{
return transport;
}
public String getType()
{
return type;
}
public String getSharedSecret()
{
if ( secretKey == null )
{
return null;
}
return new String( secretKey.getEncoded(), StandardCharsets.UTF_8 );
}
public String getRawUsername()
{
if ( credentials == null )
{
return null;
}
return credentials.getUsername();
}
String getRawPassword()
{
if ( credentials == null )
{
return null;
}
return credentials.getPassword();
}
public Credentials getCredentialsFor( JID user )
{
if ( credentials != null )
{
return credentials;
}
if ( secretKey != null )
{
final int ttl = 86400;
final Date expires = new Date( System.currentTimeMillis() + ( ttl * 1000 ) );
final String asSecondsSinceEpoch = Long.toString( expires.getTime() / 1000 );
// temporary-username="timestamp" + ":" + "username"
String username = asSecondsSinceEpoch; // The TURN server should accept usernames without an identifier-part.
if ( user != null )
{
// Try to set the JID as a username part, if we can.
try
{
// Although RFC 5389 appears to allow for unescaped JIDs to be used as TURN usernames, problems have
// been reported (eg: https://github.com/versatica/JsSIP/issues/184)111
username = URLEncoder.encode( user.toBareJID(), "ASCII" ) + ":" + asSecondsSinceEpoch;
}
catch ( UnsupportedEncodingException e )
{
Log.debug( "Unable to encode JID ''.", user.toBareJID(), e );
}
}
// temporary-password = base64_encode(hmac-sha1(input = temporary-username, key = shared-secret))
final String password;
try
{
final Mac mac = Mac.getInstance( "HmacSHA1" );
mac.init( secretKey );
final byte[] nonce = mac.doFinal( username.getBytes( StandardCharsets.UTF_8 ) );
password = StringUtils.encodeBase64( nonce );
}
catch ( InvalidKeyException | NoSuchAlgorithmException e )
{
Log.warn( "Unable to create ephemeral credentials for '{}' on {}:{}", user, host, port, e );
return null;
}
return new Credentials( username, password, expires );
}
return null;
}
@Override
public boolean equals( Object o )
{
if ( this == o )
{
return true;
}
if ( o == null || getClass() != o.getClass() )
{
return false;
}
final Service service = (Service) o;
if ( databaseId != service.databaseId )
{
return false;
}
if ( !host.equals( service.host ) )
{
return false;
}
if ( name != null ? !name.equals( service.name ) : service.name != null )
{
return false;
}
if ( port != null ? !port.equals( service.port ) : service.port != null )
{
return false;
}
if ( restricted != null ? !restricted.equals( service.restricted ) : service.restricted != null )
{
return false;
}
if ( transport != null ? !transport.equals( service.transport ) : service.transport != null )
{
return false;
}
if ( !type.equals( service.type ) )
{
return false;
}
if ( credentials != null ? !credentials.equals( service.credentials ) : service.credentials != null )
{
return false;
}
return secretKey != null ? secretKey.equals( service.secretKey ) : service.secretKey == null;
}
@Override
public int hashCode()
{
int result = (int) ( databaseId ^ ( databaseId >>> 32 ) );
result = 31 * result + host.hashCode();
result = 31 * result + ( name != null ? name.hashCode() : 0 );
result = 31 * result + ( port != null ? port.hashCode() : 0 );
result = 31 * result + ( restricted != null ? restricted.hashCode() : 0 );
result = 31 * result + ( transport != null ? transport.hashCode() : 0 );
result = 31 * result + type.hashCode();
result = 31 * result + ( credentials != null ? credentials.hashCode() : 0 );
result = 31 * result + ( secretKey != null ? secretKey.hashCode() : 0 );
return result;
}
}
/*
* Copyright (C) 2017 Ignite Realtime Foundation. 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.igniterealtime.openfire.plugins.externalservicediscovery;
import org.jivesoftware.database.DbConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Types;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A manager of {@link Service} instances.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class ServiceManager
{
private static final Logger Log = LoggerFactory.getLogger( ServiceManager.class );
private Set<Service> services = new HashSet<>();
// Not making this a singleton, to allow for database-reloads in a cluster.
public static ServiceManager getInstance()
{
final ServiceManager instance = new ServiceManager();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet resultSet = null;
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( "SELECT * FROM ofExternalServices " );
resultSet = pstmt.executeQuery();
while ( resultSet.next() )
{
final long databaseId = resultSet.getLong( "serviceID" );
String name = resultSet.getString( "name" );
if ( resultSet.wasNull() || name == null || name.isEmpty() )
{
name = null;
}
String host = resultSet.getString( "host" );
if ( resultSet.wasNull() || host == null || host.isEmpty() )
{
host = null;
}
Integer port = resultSet.getInt( "port" );
if ( resultSet.wasNull() )
{
port = null;
}
Boolean restricted = resultSet.getBoolean( "restricted" );
if ( resultSet.wasNull() )
{
restricted = null;
}
String transport = resultSet.getString( "transport" );
if ( resultSet.wasNull() || transport == null || transport.isEmpty() )
{
transport = null;
}
String type = resultSet.getString( "type" );
if ( resultSet.wasNull() || type == null || type.isEmpty() )
{
type = null;
}
String username = resultSet.getString( "username" );
if ( resultSet.wasNull() || username == null || username.isEmpty() )
{
username = null;
}
String password = resultSet.getString( "password" );
if ( resultSet.wasNull() || password == null || password.isEmpty() )
{
password = null;
}
String sharedSecret = resultSet.getString( "sharedSecret" );
if ( resultSet.wasNull() || sharedSecret == null || sharedSecret.isEmpty() )
{
sharedSecret = null;
}
final Service service = new Service( databaseId, name, host, port, restricted, transport, type, username, password, sharedSecret );
instance.services.add( service );
Log.debug( "Loaded {} service at {} from database.", service.getType(), service.getHost() );
}
}
catch ( Exception e )
{
Log.error( "Unable to load services from database!", e );
}
finally
{
DbConnectionManager.closeConnection( resultSet, pstmt, con );
}
return instance;
}
public void addService( Service service )
{
if ( services.add( service ) )
{
Connection con = null;
PreparedStatement pstmt = null;
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( "INSERT INTO ofExternalServices VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" );
pstmt.setLong( 1, service.getDatabaseId() );
if ( service.getName() == null || service.getName().isEmpty() )
{
pstmt.setNull( 2, Types.VARCHAR );
}
else
{
pstmt.setString( 2, service.getName() );
}
if ( service.getHost() == null || service.getHost().isEmpty() )
{
pstmt.setNull( 3, Types.VARCHAR );
}
else
{
pstmt.setString( 3, service.getHost() );
}
if ( service.getPort() == null )
{
pstmt.setNull( 4, Types.INTEGER );
}
else
{
pstmt.setInt( 4, service.getPort() );
}
if ( service.getRestricted() == null )
{
pstmt.setNull( 5, Types.BOOLEAN );
}
else
{
pstmt.setBoolean( 5, service.getRestricted() );
}
if ( service.getTransport() == null || service.getTransport().isEmpty() )
{
pstmt.setNull( 6, Types.CHAR );
}
else
{
pstmt.setString( 6, service.getTransport() );
}
if ( service.getType() == null || service.getType().isEmpty() )
{
pstmt.setNull( 7, Types.CHAR );
}
else
{
pstmt.setString( 7, service.getType() );
}
if ( service.getRawUsername() == null || service.getRawUsername().isEmpty() )
{
pstmt.setNull( 8, Types.VARCHAR );
}
else
{
pstmt.setString( 8, service.getRawUsername() );
}
if ( service.getRawPassword() == null || service.getRawPassword().isEmpty() )
{
pstmt.setNull( 9, Types.VARCHAR );
}
else
{
pstmt.setString( 9, service.getRawPassword() );
}
if ( service.getSharedSecret() == null || service.getSharedSecret().isEmpty() )
{
pstmt.setNull( 10, Types.VARCHAR );
}
else
{
pstmt.setString( 10, service.getSharedSecret() );
}
pstmt.executeUpdate();
Log.info( "Added {} service at {}.", service.getType(), service.getHost() );
}
catch ( Exception e )
{
Log.error( "Unable to persists service ({} at {}) in database!", service.getType(), service.getHost(), e );
services.remove( service );
}
finally
{
DbConnectionManager.closeConnection( pstmt, con );
}
}
}
public void removeService( Service service )
{
Connection con = null;
PreparedStatement pstmt = null;
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( "DELETE FROM ofExternalServices WHERE serviceID=?" );
pstmt.setLong( 1, service.getDatabaseId() );
if ( pstmt.executeUpdate() == 0 )
{
Log.warn( "The query to remove {} service at {} from the database did not remove anything.", service.getType(), service.getHost() );
}
else
{
services.remove( service );
Log.info( "Removed {} service at {}.", service.getType(), service.getHost() );
}
}
catch ( Exception e )
{
Log.error( "Unable to remove service ({} at {}) from database!", service.getType(), service.getHost(), e );
}
finally
{
DbConnectionManager.closeConnection( pstmt, con );
}
}
public Set<Service> getAllServices()
{
return new HashSet<>( services );
}
public Map<Service, Credentials> getServicesFor( JID requester )
{
Log.debug( "Obtaining credentials for {}", requester );
final Map<Service, Credentials> result = new HashMap<>();
for ( final Service service : services )
{
try
{
final Credentials credentials = service.getCredentialsFor( requester );
result.put( service, credentials );
}
catch ( Exception e )
{
Log.warn( "Unable to obtain credentials for requester '{}', for the {} service at: {}", requester, service.getType(), service.getHost(), e );
}
}
return result;
}
public Map<Service, Credentials> getServicesFor( JID requester, String requestedType )
{
Log.debug( "Obtaining credentials for {} of type {}", requester, requestedType );
final Map<Service, Credentials> result = new HashMap<>();
for ( final Service service : services )
{
if ( requestedType.equals( service.getType() ) )
{
try
{
final Credentials credentials = service.getCredentialsFor( requester );
result.put( service, credentials );
}
catch ( Exception e )
{
Log.warn( "Unable to obtain credentials for requester '{}', for the {} service at: {}", requester, service.getType(), service.getHost(), e );
}
}
}
return result;
}
public Map<Service, Credentials> getServicesFor( JID requester, String requestedHost, String requestedType )
{
Log.debug( "Obtaining credentials for {} on {} of type {}", requester, requestedHost, requestedType );
final Map<Service, Credentials> result = new HashMap<>();
for ( final Service service : services )
{
if ( requestedType.equals( service.getType() )
&& requestedHost.equals( service.getHost() ) )
{
try
{
final Credentials credentials = service.getCredentialsFor( requester );
result.put( service, credentials );
}
catch ( Exception e )
{
Log.warn( "Unable to obtain credentials for requester '{}', for the {} service at: {}", requester, service.getType(), service.getHost(), e );
}
}
}
return result;
}
public Map<Service, Credentials> getServicesFor( JID requester, String requestedHost, String requestedType, int requestedPort )
{
Log.debug( "Obtaining credentials for {} on {}:{} of type {}", requester, requestedHost, requestedPort, requestedType );
final Map<Service, Credentials> result = new HashMap<>();
for ( final Service service : services )
{
if ( requestedType.equals( service.getType() )
&& requestedHost.equals( service.getHost() )
&& requestedPort == service.getPort() )
{
try
{
final Credentials credentials = service.getCredentialsFor( requester );
result.put( service, credentials );
}
catch ( Exception e )
{
Log.warn( "Unable to obtain credentials for requester '{}', for the {} service at: {}:{}", requester, requestedType, requestedHost, requestedPort, e );
}
}
}
return result;
}
}
package org.igniterealtime.openfire.plugins.externalservicediscovery;
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.Test;
/**
* Simple tests that verify the implementation of {@link Service}.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class ServiceTest
{
/**
* Happy flow - should not cause any issues.
*/
@Test
public void testPackageConstructor()
{
int databaseId = -1;
String name = "description";
String host = "host";
Integer port = 123;
Boolean restricted = false;
String transport = "udp";
String type = "turn";
String username = "username";
String password = "password";
String sharedSecret = "secret";
new Service( databaseId, name, host, port, restricted, transport, type, username, password, sharedSecret );
}
/**
* Verifies that an IllegalArgumentException is thrown by the constructor when the provided 'host' argument value is null.
*/
@Test( expected = IllegalArgumentException.class )
public void testNullHost()
{
int databaseId = -1;
String name = "description";
String host = null;
Integer port = 123;
Boolean restricted = false;
String transport = "udp";
String type = "turn";
String username = "username";
String password = "password";
String sharedSecret = "secret";
new Service( databaseId, name, host, port, restricted, transport, type, username, password, sharedSecret );
}
/**
* Verifies that an IllegalArgumentException is thrown by the constructor when the provided 'type' argument value is null.
*/
@Test( expected = IllegalArgumentException.class )
public void testNullType()
{
int databaseId = -1;
String name = "description";
String host = "host";
Integer port = 123;
Boolean restricted = false;
String transport = "udp";
String type = null;
String username = "username";
String password = "password";
String sharedSecret = "secret";
new Service( databaseId, name, host, port, restricted, transport, type, username, password, sharedSecret );
}
/**
* Verifies that the implementation adheres to the contract specified in {@link Object#equals(Object)}.
*/
@Test
public void equalsContract()
{
EqualsVerifier.forClass( Service.class )
.withNonnullFields( "host", "type" ) // checked by constructor.
.verify();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
</web-app>
<!--
- Copyright (C) 2017 Ignite Realtime Foundation. 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.
-->
<%@ page errorPage="error.jsp" %>
<%@ page import="org.igniterealtime.openfire.plugins.externalservicediscovery.Service" %>
<%@ page import="org.igniterealtime.openfire.plugins.externalservicediscovery.ServiceManager" %>
<%@ page import="org.jivesoftware.util.CookieUtils" %>
<%@ page import="org.jivesoftware.util.ParamUtils" %>
<%@ page import="org.jivesoftware.util.StringUtils" %>
<%@ page import="java.io.UnsupportedEncodingException" %>
<%@ page import="java.net.URLDecoder" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.jivesoftware.database.SequenceManager" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="admin" prefix="admin" %>
<jsp:useBean id="webManager" class="org.jivesoftware.util.WebManager" />
<% webManager.init(request, response, session, application, out ); %>
<%!
public String getDecodedParameter( HttpServletRequest request, String name ) throws UnsupportedEncodingException
{
String value = request.getParameter( name );
if ( value != null )
{
return URLDecoder.decode( value.trim(), "UTF-8" );
}
else
{
return null;
}
}
%>
<%
String deleteService = getDecodedParameter( request, "deleteService" );
String addService = getDecodedParameter( request, "addService" );
String name = getDecodedParameter( request, "name" );
String host = getDecodedParameter( request, "host" );
String port = getDecodedParameter( request, "port" );
String transport = getDecodedParameter( request, "transport" );
String type = getDecodedParameter( request, "type" );
String credentials = getDecodedParameter( request, "credentials" );
String username = getDecodedParameter( request, "username" );
String password = getDecodedParameter( request, "password" );
String secret = getDecodedParameter( request, "secret" );
Map<String, String> errors = new HashMap<>();
final Cookie csrfCookie = CookieUtils.getCookie( request, "csrf");
String csrfParam = ParamUtils.getParameter( request, "csrf");
if (deleteService != null || addService != null) {
if (csrfCookie == null || csrfParam == null || !csrfCookie.getValue().equals(csrfParam)) {
deleteService = null;
addService = null;
errors.put( "csrf", "Invalid CSRF value. Reload the page and try again." );
}
}
csrfParam = StringUtils.randomString( 15 );
CookieUtils.setCookie(request, response, "csrf", csrfParam, -1);
pageContext.setAttribute("csrf", csrfParam);
if ( errors.isEmpty() )
{
if ( addService != null )
{
if ( host == null || host.isEmpty() )
{
errors.put( "host", "empty" );
}
if ( type == null || type.isEmpty() )
{
errors.put( "type", "empty" );
}
Integer portValue = null;
if ( port != null && !port.isEmpty() )
{
try
{
portValue = Integer.parseInt( port );
}
catch ( NumberFormatException e )
{
errors.put( "port", "not a number" );
}
}
if ( "userpass".equals( credentials ) )
{
if ( username == null || username.isEmpty() )
{
errors.put( "username", "empty" );
}
if ( password == null || password.isEmpty() )
{
errors.put( "password", "empty" );
}
}
if ( "sharedsecret".equals( credentials ) )
{
if ( secret == null || secret.isEmpty() )
{
errors.put( "sharedsecret", "empty" );
}
}
if ( errors.isEmpty() )
{
final Service service;
if ( "userpass".equals( credentials ) )
{
service = new Service( name, host, portValue, transport, type, username, password );
}
else if ( "sharedsecret".equals( credentials ) )
{
service = new Service( name, host, portValue, transport, type, secret );
}
else
{
service = new Service( name, host, portValue, transport, type );
}
ServiceManager.getInstance().addService( service );
response.sendRedirect( "external-service-discovery-config.jsp?success=addService" );
return;
}
}
if ( deleteService != null )
{
for ( final Service service : ServiceManager.getInstance().getAllServices() )
{
if ( Long.toString( service.getDatabaseId() ).equals( deleteService ) )
{
ServiceManager.getInstance().removeService( service );
response.sendRedirect("external-service-discovery-config.jsp?success=deleteService");
return;
}
}
}
}
pageContext.setAttribute( "errors", errors );
pageContext.setAttribute( "services", ServiceManager.getInstance().getAllServices() );
%>
<html>
<head>
<title><fmt:message key="external-service-discovery-config.title"/></title>
<meta name="pageID" content="external-service-discovery-config"/>
<script>
function check( id )
{
document.getElementById('nocredentials').checked = ( id === 'nocredentials' );
document.getElementById('userpass').checked = ( id === 'userpass' );
document.getElementById('sharedsecret').checked = ( id === 'sharedsecret' );
}
</script>
</head>
<body>
<c:forEach var="err" items="${errors}">
<admin:infobox type="error">
<c:choose>
<c:when test="${err.key eq 'csrf'}">
<fmt:message key="global.csrf.failed" />
</c:when>
<c:when test="${err.key eq 'host'}">
<fmt:message key="external-service-discovery-config.host-required"/>
</c:when>
<c:when test="${err.key eq 'type'}">
<fmt:message key="external-service-discovery-config.type-required"/>
</c:when>
<c:when test="${err.key eq 'port'}">
<fmt:message key="external-service-discovery-config.port-number"/>
</c:when>
<c:when test="${err.key eq 'username'}">
<fmt:message key="external-service-discovery-config.credentials.username-required"/>
</c:when>
<c:when test="${err.key eq 'password'}">
<fmt:message key="external-service-discovery-config.credentials.password-required"/>
</c:when>
<c:when test="${err.key eq 'sharedsecret'}">
<fmt:message key="external-service-discovery-config.credentials.sharedsecret-required"/>
</c:when>
<c:otherwise>
<c:if test="${not empty err.value}">
<fmt:message key="admin.error"/>: <c:out value="${err.value}"/>
</c:if>
(<c:out value="${err.key}"/>)
</c:otherwise>
</c:choose>
</admin:infobox>
</c:forEach>
<!-- Display success report, but only if there were no errors. -->
<c:if test="${not empty param['success'] and empty errors}">
<admin:infoBox type="success">
<c:choose>
<c:when test="${param['success'] eq 'deleteService'}">
<fmt:message key="external-service-discovery-config.success-delete"/>
</c:when>
<c:when test="${param['success'] eq 'addService'}">
<fmt:message key="external-service-discovery-config.success-add"/>
</c:when>
</c:choose>
</admin:infoBox>
</c:if>
<p>
<fmt:message key="external-service-discovery-config.descr"/>
</p>
<div class="jive-table">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<thead>
<tr>
<th>&nbsp;</th>
<th nowrap><fmt:message key="external-service-discovery-config.host" /></th>
<th nowrap><fmt:message key="external-service-discovery-config.port" /></th>
<th nowrap><fmt:message key="external-service-discovery-config.name" /></th>
<th nowrap><fmt:message key="external-service-discovery-config.transport" /></th>
<th nowrap><fmt:message key="external-service-discovery-config.type" /></th>
<th nowrap><fmt:message key="external-service-discovery-config.credentials" /></th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<c:choose>
<c:when test="${empty services}">
<tr>
<td align="center" colspan="7"><fmt:message key="external-service-discovery-config.table.empty" /></td>
</tr>
</c:when>
<c:otherwise>
<c:forEach var="service" items="${services}">
<tr>
<td>&nbsp;</td>
<td><c:out value="${service.host}"/></td>
<td><c:out value="${service.port}"/></td>
<td><c:out value="${service.name}"/></td>
<td><c:out value="${service.transport}"/></td>
<td><c:out value="${service.type}"/></td>
<td>
<c:choose>
<c:when test="${not empty service.rawUsername}">
<fmt:message key="external-service-discovery-config.credentials.using-userpass" /> <c:out value="service.rawUsername"/>
</c:when>
<c:when test="${not empty service.sharedSecret}">
<fmt:message key="external-service-discovery-config.credentials.using-sharedsecret" />
</c:when>
<c:otherwise>
<fmt:message key="external-service-discovery-config.credentials.using-nocredentials" />
</c:otherwise>
</c:choose>
</td>
<td width="1%" align="center">
<a href="external-service-discovery-config.jsp?deleteService=${service.databaseId}&csrf=${csrf}" title="<fmt:message key="global.click_delete" />"
><img src="images/delete-16x16.gif" width="16" height="16" border="0" alt="<fmt:message key="global.click_delete" />"></a>
</td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</tbody>
</table>
</div>
<br/>
<p><fmt:message key="external-service-discovery-config.add-service.descr"/></p>
<form action="external-service-discovery-config.jsp">
<fmt:message key="external-service-discovery-config.add-service.title" var="add_title"/>
<admin:contentBox title="${add_title}">
<input type="hidden" name="csrf" value="${csrf}">
<table cellpadding="0" cellspacing="0" border="0" >
<tr>
<td nowrap><fmt:message key="external-service-discovery-config.host" />*:</td>
<td><input type="text" id="host" name="host" size="50" maxlength="255"></td>
</tr>
<tr>
<td nowrap><fmt:message key="external-service-discovery-config.port" />:</td>
<td><input type="text" id="port" name="port" size="7" maxlength="5"></td>
</tr>
<tr>
<td nowrap><fmt:message key="external-service-discovery-config.name" />:</td>
<td><input type="text" id="name" name="name" size="75" maxlength="255"></td>
</tr>
<tr>
<td nowrap><fmt:message key="external-service-discovery-config.transport" />:</td>
<td>
<input type="radio" id="tcp" name="transport" value="tcp"><label for="tcp"><fmt:message key="external-service-discovery-config.tcp"/></label>
</td>
</tr>
<tr>
<td nowrap>&nbsp;</td>
<td>
<input type="radio" id="udp" name="transport" value="tcp"><label for="udp"><fmt:message key="external-service-discovery-config.udp"/></label>
</td>
</tr>
<tr>
<td nowrap><fmt:message key="external-service-discovery-config.type" />*:</td>
<td>
<input type="radio" id="stun" name="type" value="stun"><label for="stun"><fmt:message key="external-service-discovery-config.stun"/></label>
</td>
</tr>
<tr>
<td nowrap>&nbsp;</td>
<td>
<input type="radio" id="turn" name="type" value="turn"><label for="turn"><fmt:message key="external-service-discovery-config.turn"/></label>
</td>
</tr>
<tr>
<td nowrap>&nbsp;</td>
<td>
<input type="radio" id="turns" name="type" value="turns"><label for="turns"><fmt:message key="external-service-discovery-config.turns"/></label>
</td>
</tr>
<tr>
<td><fmt:message key="external-service-discovery-config.credentials"/>*:</td>
<td>
<input type="radio" id="nocredentials" name="credentials" value="nocredentials" checked><label for="nocredentials"><fmt:message key="external-service-discovery-config.credentials.nocredentials"/></label>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="radio" id="userpass" name="credentials" value="userpass"><label for="userpass"><fmt:message key="external-service-discovery-config.credentials.userpass"/></label>
<label for="username"><fmt:message key="external-service-discovery-config.credentials.username"/></label>: <input type="text" id="username" name="username" size="25" maxlength="255" onclick="check('userpass')">
<label for="password"><fmt:message key="external-service-discovery-config.credentials.password"/></label>: <input type="password" id="password" name="password" size="25" maxlength="1024" onclick="check('userpass')">
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="radio" id="sharedsecret" name="credentials" value="sharedsecret"><label for="sharedsecret"><fmt:message key="external-service-discovery-config.credentials.sharedsecret"/></label>
<input type="text" id="secret" name="secret" size="40" maxlength="1024" onclick="check('sharedsecret')">
</td>
</tr>
</table>
</admin:contentBox>
<input type="submit" name="addService" value="<fmt:message key="external-service-discovery-config.add-service.button-caption" />">
</form>
</body>
</html>
......@@ -105,6 +105,7 @@
<module>dbaccess</module>
<module>emailListener</module>
<module>emailOnAway</module>
<module>externalservicediscovery</module>
<module>fastpath</module>
<module>gojara</module>
<module>hazelcast</module>
......
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