/*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.eclipse.paho.client.mqttv3.internal;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ServiceLoader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.logging.Logger;
import org.eclipse.paho.client.mqttv3.logging.LoggerFactory;
import org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory;
/**
* The NetworkModuleService uses the installed {@link NetworkModuleFactory}s to create {@link NetworkModule} instances.
*
* The selection of the appropriate NetworkModuleFactory is based on the URI scheme.
*
* @author Maik Scheibler
*/
public class NetworkModuleService {
private static Logger LOG = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT,
NetworkModuleService.class.getSimpleName());
private static final ServiceLoader FACTORY_SERVICE_LOADER = ServiceLoader.load(
NetworkModuleFactory.class, NetworkModuleService.class.getClassLoader());
/** Pattern to match URI authority parts: {@code authority = [userinfo"@"]host[":"port]} */
private static final Pattern AUTHORITY_PATTERN = Pattern.compile("((.+)@)?([^:]*)(:(\\d+))?");
private static final int AUTH_GROUP_USERINFO = 2;
private static final int AUTH_GROUP_HOST = 3;
private static final int AUTH_GROUP_PORT = 5;
private NetworkModuleService() {
// no instances
}
/**
* Validates the provided URI to be valid and that a NetworkModule is installed to serve it.
*
* @param brokerUri to be validated
* @throws IllegalArgumentException is case the URI is invalid or there is no {@link NetworkModule} installed for
* the URI scheme
*/
public static void validateURI(String brokerUri) throws IllegalArgumentException {
try {
URI uri = new URI(brokerUri);
String scheme = uri.getScheme();
if (scheme == null || scheme.isEmpty()) {
throw new IllegalArgumentException("missing scheme in broker URI: " + brokerUri);
}
scheme = scheme.toLowerCase();
for (NetworkModuleFactory factory : FACTORY_SERVICE_LOADER) {
if (factory.getSupportedUriSchemes().contains(scheme)) {
factory.validateURI(uri);
return;
}
}
throw new IllegalArgumentException("no NetworkModule installed for scheme \"" + scheme
+ "\" of URI \"" + brokerUri + "\"");
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Can't parse string to URI \"" + brokerUri + "\"", e);
}
}
/**
* Creates a {@link NetworkModule} instance for the provided address, using the given options.
*
* @param address must be a valid URI
* @param options used to initialize the NetworkModule
* @param clientId a client identifier that is unique on the server being connected to
* @return a new NetworkModule instance
* @throws MqttException if the initialization fails
* @throws IllegalArgumentException if the provided {@code address} is invalid
*/
public static NetworkModule createInstance(String address, MqttConnectOptions options, String clientId)
throws MqttException, IllegalArgumentException
{
try {
URI brokerUri = new URI(address);
applyRFC3986AuthorityPatch(brokerUri);
String scheme = brokerUri.getScheme().toLowerCase();
for (NetworkModuleFactory factory : FACTORY_SERVICE_LOADER) {
if (factory.getSupportedUriSchemes().contains(scheme)) {
return factory.createNetworkModule(brokerUri, options, clientId);
}
}
/*
* To throw an IllegalArgumentException exception matches the previous behavior of
* MqttConnectOptions.validateURI(String), but it would be nice to provide something more meaningful.
*/
throw new IllegalArgumentException(brokerUri.toString());
} catch (URISyntaxException e) {
throw new IllegalArgumentException(address, e);
}
}
/**
* Java does URI parsing according to RFC2396 and thus hostnames are limited to alphanumeric characters and '-'.
* But the current "Uniform Resource Identifier (URI): Generic Syntax" (RFC3986) allows for a much wider
* range of valid characters. This causes Java to fail parsing the authority part and thus the user-info, host and
* port will not be set on an URI which does not conform to RFC2396.
*
* This workaround tries to detect such a parsing failure and does tokenize the authority parts according to
* RFC3986, but does not enforce any character restrictions (for sake of simplicity).
*
* @param toPatch - The URI To patch
* @see rfc3986 - section-3.2
*/
public static void applyRFC3986AuthorityPatch(URI toPatch) {
if (toPatch == null
|| toPatch.getHost() != null // already successfully parsed
|| toPatch.getAuthority() == null
|| toPatch.getAuthority().isEmpty())
{
return;
}
Matcher matcher = AUTHORITY_PATTERN.matcher(toPatch.getAuthority());
if (matcher.find()) {
setURIField(toPatch, "userInfo", matcher.group(AUTH_GROUP_USERINFO));
setURIField(toPatch, "host", matcher.group(AUTH_GROUP_HOST));
String portString = matcher.group(AUTH_GROUP_PORT);
setURIField(toPatch, "port", portString != null ? Integer.parseInt(portString) : -1);
}
}
/**
* Reflective manipulation of a URI field to work around the URI parser, because all fields are validated even on
* the full qualified URI constructor.
*
* @see URI#URI(String, String, String, int, String, String, String)
*/
private static void setURIField(URI toManipulate, String fieldName, Object newValue) {
try {
Field field = URI.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(toManipulate, newValue);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
LOG.warning(NetworkModuleService.class.getName(), "setURIField", "115", new Object[] {
toManipulate.toString() }, e);
}
}
}