blob: 0cd201dbece2fc64881c0e33f772d7c6bfade5b1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.druid.utils;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import org.apache.druid.java.util.common.IAE;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
public final class ConnectionUriUtils
{
private static final String MARIADB_EXTRAS = "nonMappedOptions";
// Note: MySQL JDBC connector 8 supports 7 other protocols than just `jdbc:mysql:`
// (https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html).
// We should consider either expanding recognized mysql protocols or restricting allowed protocols to
// just a basic one.
public static final String MYSQL_PREFIX = "jdbc:mysql:";
public static final String POSTGRES_PREFIX = "jdbc:postgresql:";
public static final String MARIADB_PREFIX = "jdbc:mariadb:";
public static final String POSTGRES_DRIVER = "org.postgresql.Driver";
public static final String MYSQL_DRIVER = "com.mysql.jdbc.Driver";
public static final String MYSQL_NON_REGISTERING_DRIVER = "com.mysql.jdbc.NonRegisteringDriver";
public static final String MARIADB_DRIVER = "org.mariadb.jdbc.Driver";
/**
* This method checks {@param actualProperties} against {@param allowedProperties} if they are not system properties.
* A property is regarded as a system property if its name starts with a prefix in {@param systemPropertyPrefixes}.
* See org.apache.druid.server.initialization.JDBCAccessSecurityConfig for more details.
*
* If a non-system property that is not allowed is found, this method throws an {@link IllegalArgumentException}.
*/
public static void throwIfPropertiesAreNotAllowed(
Set<String> actualProperties,
Set<String> systemPropertyPrefixes,
Set<String> allowedProperties
)
{
for (String property : actualProperties) {
if (systemPropertyPrefixes.stream().noneMatch(property::startsWith)) {
Preconditions.checkArgument(
allowedProperties.contains(property),
"The property [%s] is not in the allowed list %s",
property,
allowedProperties
);
}
}
}
/**
* This method tries to determine the correct type of database for a given JDBC connection string URI, then load the
* driver using reflection to parse the uri parameters, returning the set of keys which can be used for JDBC
* parameter whitelist validation.
*
* uris starting with {@link #MYSQL_PREFIX} will first try to use the MySQL Connector/J driver (5.x), then fallback
* to MariaDB Connector/J (version 2.x) which also accepts jdbc:mysql uris. This method does not attempt to use
* MariaDB Connector/J 3.x alpha driver (at the time of these javadocs, it only handles the jdbc:mariadb prefix)
*
* uris starting with {@link #POSTGRES_PREFIX} will use the postgresql driver to parse the uri
*
* uris starting with {@link #MARIADB_PREFIX} will first try to use MariaDB Connector/J driver (2.x) then fallback to
* MariaDB Connector/J 3.x driver.
*
* If the uri does not match any of these schemes, this method will return an empty set if unknown uris are allowed,
* or throw an exception if not.
*/
public static Set<String> tryParseJdbcUriParameters(String connectionUri, boolean allowUnknown)
{
if (connectionUri.startsWith(MYSQL_PREFIX)) {
try {
return tryParseMySqlConnectionUri(connectionUri);
}
catch (ClassNotFoundException notFoundMysql) {
try {
return tryParseMariaDb2xConnectionUri(connectionUri);
}
catch (ClassNotFoundException notFoundMaria2x) {
throw new RuntimeException(
"Failed to find MySQL driver class. Please check the MySQL connector version 5.1.48 is in the classpath",
notFoundMysql
);
}
catch (IllegalArgumentException iaeMaria2x) {
throw iaeMaria2x;
}
catch (Throwable otherMaria2x) {
throw new RuntimeException(otherMaria2x);
}
}
catch (IllegalArgumentException iaeMySql) {
throw iaeMySql;
}
catch (Throwable otherMysql) {
throw new RuntimeException(otherMysql);
}
} else if (connectionUri.startsWith(MARIADB_PREFIX)) {
try {
return tryParseMariaDb2xConnectionUri(connectionUri);
}
catch (ClassNotFoundException notFoundMaria2x) {
try {
return tryParseMariaDb3xConnectionUri(connectionUri);
}
catch (ClassNotFoundException notFoundMaria3x) {
throw new RuntimeException(
"Failed to find MariaDB driver class. Please check the MariaDB connector version 2.7.3 is in the classpath",
notFoundMaria2x
);
}
catch (IllegalArgumentException iaeMaria3x) {
throw iaeMaria3x;
}
catch (Throwable otherMaria3x) {
throw new RuntimeException(otherMaria3x);
}
}
catch (IllegalArgumentException iaeMaria2x) {
throw iaeMaria2x;
}
catch (Throwable otherMaria2x) {
throw new RuntimeException(otherMaria2x);
}
} else if (connectionUri.startsWith(POSTGRES_PREFIX)) {
try {
return tryParsePostgresConnectionUri(connectionUri);
}
catch (IllegalArgumentException iaePostgres) {
throw iaePostgres;
}
catch (Throwable otherPostgres) {
// no special handling for class not found because postgres driver is in distribution and should be available.
throw new RuntimeException(otherPostgres);
}
} else {
if (!allowUnknown) {
throw new IAE("Unknown JDBC connection scheme: %s", connectionUri.split(":")[1]);
}
return Collections.emptySet();
}
}
public static Set<String> tryParsePostgresConnectionUri(String connectionUri)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
{
Class<?> driverClass = Class.forName(POSTGRES_DRIVER);
Method parseUrl = driverClass.getMethod("parseURL", String.class, Properties.class);
Properties properties = (Properties) parseUrl.invoke(null, connectionUri, null);
if (properties == null) {
throw new IAE("Invalid URL format for PostgreSQL: [%s]", connectionUri);
}
Set<String> keys = Sets.newHashSetWithExpectedSize(properties.size());
properties.forEach((k, v) -> keys.add((String) k));
return keys;
}
public static Set<String> tryParseMySqlConnectionUri(String connectionUri)
throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException,
InvocationTargetException
{
Class<?> driverClass = Class.forName(MYSQL_NON_REGISTERING_DRIVER);
Method parseUrl = driverClass.getMethod("parseURL", String.class, Properties.class);
// almost the same as postgres, but is an instance level method
Properties properties = (Properties) parseUrl.invoke(driverClass.getConstructor().newInstance(), connectionUri, null);
if (properties == null) {
throw new IAE("Invalid URL format for MySQL: [%s]", connectionUri);
}
Set<String> keys = Sets.newHashSetWithExpectedSize(properties.size());
properties.forEach((k, v) -> keys.add((String) k));
return keys;
}
public static Set<String> tryParseMariaDb2xConnectionUri(String connectionUri)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException,
NoSuchFieldException, InstantiationException
{
// these are a bit more complicated
Class<?> urlParserClass = Class.forName("org.mariadb.jdbc.UrlParser");
Class<?> optionsClass = Class.forName("org.mariadb.jdbc.util.Options");
Method parseUrl = urlParserClass.getMethod("parse", String.class);
Method getOptions = urlParserClass.getMethod("getOptions");
Object urlParser = parseUrl.invoke(null, connectionUri);
if (urlParser == null) {
throw new IAE("Invalid URL format for MariaDB: [%s]", connectionUri);
}
Object options = getOptions.invoke(urlParser);
Field nonMappedOptionsField = optionsClass.getField(MARIADB_EXTRAS);
Properties properties = (Properties) nonMappedOptionsField.get(options);
Field[] fields = optionsClass.getDeclaredFields();
Set<String> keys = Sets.newHashSetWithExpectedSize(properties.size() + fields.length);
properties.forEach((k, v) -> keys.add((String) k));
Object defaultOptions = optionsClass.getConstructor().newInstance();
for (Field field : fields) {
if (field.getName().equals(MARIADB_EXTRAS)) {
continue;
}
try {
if (!Objects.equal(field.get(options), field.get(defaultOptions))) {
keys.add(field.getName());
}
}
catch (IllegalAccessException ignored) {
// ignore stuff we aren't allowed to read
}
}
return keys;
}
public static Set<String> tryParseMariaDb3xConnectionUri(String connectionUri)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException,
InstantiationException
{
Class<?> configurationClass = Class.forName("org.mariadb.jdbc.Configuration");
Class<?> configurationBuilderClass = Class.forName("org.mariadb.jdbc.Configuration$Builder");
Method parseUrl = configurationClass.getMethod("parse", String.class);
Method buildMethod = configurationBuilderClass.getMethod("build");
Object configuration = parseUrl.invoke(null, connectionUri);
if (configuration == null) {
throw new IAE("Invalid URL format for MariaDB: [%s]", connectionUri);
}
Method nonMappedOptionsGetter = configurationClass.getMethod(MARIADB_EXTRAS);
Properties properties = (Properties) nonMappedOptionsGetter.invoke(configuration);
Field[] fields = configurationClass.getDeclaredFields();
Set<String> keys = Sets.newHashSetWithExpectedSize(properties.size() + fields.length);
properties.forEach((k, v) -> keys.add((String) k));
Object defaultConfiguration = buildMethod.invoke(configurationBuilderClass.getConstructor().newInstance());
for (Field field : fields) {
if (field.getName().equals(MARIADB_EXTRAS)) {
continue;
}
try {
final Method fieldGetter = configurationClass.getMethod(field.getName());
if (!Objects.equal(fieldGetter.invoke(configuration), fieldGetter.invoke(defaultConfiguration))) {
keys.add(field.getName());
}
}
catch (IllegalAccessException | NoSuchMethodException ignored) {
// ignore stuff we aren't allowed to read
}
}
return keys;
}
private ConnectionUriUtils()
{
}
}