/* * 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); } } }