SQL: improve validation of properties (both names and values) (elastic/x-pack-elasticsearch#2759)

* refactor Configuration class

move away from Properties
perform validation of settings names and values at startup
better exception handling

Original commit: elastic/x-pack-elasticsearch@d8a9edeccf
This commit is contained in:
Costin Leau 2017-10-27 22:54:03 +03:00 committed by GitHub
parent af591b9edd
commit 8d5572a77b
9 changed files with 167 additions and 77 deletions

View File

@ -14,8 +14,11 @@ import java.net.URL;
import java.sql.DriverPropertyInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
//
@ -41,41 +44,57 @@ public class JdbcConfiguration extends ConnectionConfiguration {
static final String DEBUG_OUTPUT_DEFAULT = "err";
static final String TIME_ZONE = "timezone";
// follow the JDBC spec and use the JVM default...
// to avoid inconsistency, the default is picked up once at startup and reused across connections
// to cater to the principle of least surprise
// really, the way to move forward is to specify a calendar or the timezone manually
static final String TIME_ZONE_DEFAULT = TimeZone.getDefault().getID();
private static final List<String> KNOWN_OPTIONS = Arrays.asList(DEBUG, DEBUG_OUTPUT, TIME_ZONE);
// options that don't change at runtime
private static final Set<String> OPTION_NAMES = new LinkedHashSet<>(Arrays.asList(TIME_ZONE, DEBUG, DEBUG_OUTPUT));
private HostAndPort hostAndPort;
private String originalUrl;
private String urlFile = "/";
// immutable properties
private final HostAndPort hostAndPort;
private final String originalUrl;
private final String urlFile;
private final boolean debug;
private final String debugOut;
private boolean debug = false;
private String debugOut = DEBUG_OUTPUT_DEFAULT;
// mutable ones
private TimeZone timeZone;
public JdbcConfiguration(String u, Properties props) throws JdbcSQLException {
super(props);
originalUrl = u;
parseUrl(u);
public static JdbcConfiguration create(String u, Properties props) throws JdbcSQLException {
Object[] result = parseUrl(u);
Properties set = settings();
debug = Boolean.parseBoolean(set.getProperty(DEBUG, DEBUG_DEFAULT));
debugOut = settings().getProperty(DEBUG_OUTPUT, DEBUG_OUTPUT_DEFAULT);
timeZone = TimeZone.getTimeZone(settings().getProperty(TIME_ZONE, TIME_ZONE_DEFAULT));
String urlFile = (String) result[0];
HostAndPort hostAndPort = (HostAndPort) result[1];
Properties urlProps = (Properties) result[2];
// override properties set in the URL with the ones specified programmatically
if (props != null) {
urlProps.putAll(props);
}
try {
return new JdbcConfiguration(u, urlFile, hostAndPort, urlProps);
} catch (JdbcSQLException e) {
throw e;
} catch (Exception ex) {
throw new JdbcSQLException(ex, ex.getMessage());
}
}
private void parseUrl(String u) throws JdbcSQLException {
private static Object[] parseUrl(String u) throws JdbcSQLException {
String url = u;
String format = "jdbc:es://[host[:port]]*/[prefix]*[?[option=value]&]*";
if (!canAccept(u)) {
throw new JdbcSQLException("Expected [" + URL_PREFIX + "] url, received [" + u +"]");
}
String urlFile = "/";
HostAndPort destination;
Properties props = new Properties();
try {
if (u.endsWith("/")) {
u = u.substring(0, u.length() - 1);
@ -141,14 +160,15 @@ public class JdbcConfiguration extends ConnectionConfiguration {
String host = hostAndPort.substring(0, index);
String port = hostAndPort.substring(index + 1);
this.hostAndPort = new HostAndPort(host, Integer.parseInt(port));
destination = new HostAndPort(host, Integer.parseInt(port));
} else {
this.hostAndPort = new HostAndPort(hostAndPort);
destination = new HostAndPort(hostAndPort);
}
//
// parse params
//
if (params != null) {
// parse properties
List<String> prms = StringUtils.tokenize(params, "&");
@ -157,13 +177,8 @@ public class JdbcConfiguration extends ConnectionConfiguration {
if (args.size() != 2) {
throw new JdbcSQLException("Invalid parameter [" + param + "], format needs to be key=value");
}
String pName = args.get(0);
if (!KNOWN_OPTIONS.contains(pName)) {
throw new JdbcSQLException("Unknown parameter [" + pName + "] ; did you mean " +
StringUtils.findSimiliar(pName, KNOWN_OPTIONS));
}
settings().setProperty(args.get(0), args.get(1));
// further validation happens in the constructor (since extra properties might be specified either way)
props.setProperty(args.get(0).trim(), args.get(1).trim());
}
}
} catch (JdbcSQLException e) {
@ -172,10 +187,31 @@ public class JdbcConfiguration extends ConnectionConfiguration {
// Add the url to unexpected exceptions
throw new IllegalArgumentException("Failed to parse acceptable jdbc url [" + u + "]", e);
}
return new Object[] { urlFile, destination, props };
}
// constructor is private to force the use of a factory in order to catch and convert any validation exception
// and also do input processing as oppose to handling this from the constructor (which is tricky or impossible)
private JdbcConfiguration(String u, String urlFile, HostAndPort hostAndPort, Properties props) throws JdbcSQLException {
super(props);
this.originalUrl = u;
this.urlFile = urlFile;
this.hostAndPort = hostAndPort;
this.debug = parseValue(DEBUG, props.getProperty(DEBUG, DEBUG_DEFAULT), Boolean::parseBoolean);
this.debugOut = props.getProperty(DEBUG_OUTPUT, DEBUG_OUTPUT_DEFAULT);
this.timeZone = parseValue(TIME_ZONE, props.getProperty(TIME_ZONE, TIME_ZONE_DEFAULT), TimeZone::getTimeZone);
}
@Override
protected Collection<? extends String> extraOptions() {
return OPTION_NAMES;
}
public URL asUrl() throws JdbcSQLException {
// TODO: need to assemble all the various params here
try {
return new URL(isSSLEnabled() ? "https" : "http", hostAndPort.ip, port(), urlFile);
} catch (MalformedURLException ex) {
@ -199,8 +235,8 @@ public class JdbcConfiguration extends ConnectionConfiguration {
return timeZone;
}
public void timeZone(TimeZone tz) {
timeZone = tz;
public void timeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
public static boolean canAccept(String url) {
@ -209,7 +245,7 @@ public class JdbcConfiguration extends ConnectionConfiguration {
public DriverPropertyInfo[] driverPropertyInfo() {
List<DriverPropertyInfo> info = new ArrayList<>();
for (String option : KNOWN_OPTIONS) {
for (String option : OPTION_NAMES) {
String value = null;
DriverPropertyInfo prop = new DriverPropertyInfo(option, value);
info.add(prop);

View File

@ -7,12 +7,10 @@ package org.elasticsearch.xpack.sql.jdbc.jdbc;
import org.elasticsearch.xpack.sql.jdbc.debug.Debug;
import org.elasticsearch.xpack.sql.jdbc.net.client.JdbcHttpClient;
import org.elasticsearch.xpack.sql.net.client.util.StringUtils;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.ClientInfoStatus;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@ -33,8 +31,6 @@ import java.util.TimeZone;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import static java.util.Collections.singletonMap;
/**
* Implementation of {@link Connection} for Elasticsearch.
*/
@ -47,7 +43,6 @@ public class JdbcConnection implements Connection, JdbcWrapper {
private boolean closed = false;
private String catalog;
private String schema;
private Properties clientInfo = new Properties();
public JdbcConnection(JdbcConfiguration connectionInfo) throws SQLException {
cfg = connectionInfo;
@ -331,42 +326,36 @@ public class JdbcConnection implements Connection, JdbcWrapper {
private void checkOpenClientInfo() throws SQLClientInfoException {
if (isClosed()) {
throw new SQLClientInfoException();
throw new SQLClientInfoException("Connection closed", null);
}
}
@Override
public void setClientInfo(String name, String value) throws SQLClientInfoException {
checkOpenClientInfo();
if (!StringUtils.hasText(name)) {
throw new SQLClientInfoException(singletonMap(name, ClientInfoStatus.REASON_VALUE_INVALID));
}
if (value != null) {
clientInfo.put(name, value);
}
else {
clientInfo.remove(name);
}
// no-op
throw new SQLClientInfoException("Unsupported operation", null);
}
@Override
public void setClientInfo(Properties properties) throws SQLClientInfoException {
checkOpenClientInfo();
clientInfo.putAll(properties);
// no-op
throw new SQLClientInfoException("Unsupported operation", null);
}
@Override
public String getClientInfo(String name) throws SQLException {
checkOpenClientInfo();
return clientInfo.getProperty(name);
// we don't support client info - the docs indicate we should return null if properties are not supported
return null;
}
@Override
public Properties getClientInfo() throws SQLException {
checkOpenClientInfo();
Properties clone = new Properties();
clone.putAll(clientInfo);
return clone;
// similar to getClientInfo - return an empty object instead of an exception
return new Properties();
}
@Override

View File

@ -1124,7 +1124,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper {
@Override
public ResultSet getClientInfoProperties() throws SQLException {
throw new UnsupportedOperationException("Client info not implemented yet");
throw new SQLException("Client info not implemented yet");
}
@Override

View File

@ -81,7 +81,7 @@ public class JdbcDriver implements java.sql.Driver, Closeable {
}
private static JdbcConfiguration initCfg(String url, Properties props) throws JdbcSQLException {
JdbcConfiguration ci = new JdbcConfiguration(url, props);
JdbcConfiguration ci = JdbcConfiguration.create(url, props);
// if there's a timeout set on the DriverManager, make sure to use it
if (DriverManager.getLoginTimeout() > 0) {
@ -100,7 +100,7 @@ public class JdbcDriver implements java.sql.Driver, Closeable {
if (!acceptsURL(url)) {
return new DriverPropertyInfo[0];
}
return new JdbcConfiguration(url, info).driverPropertyInfo();
return JdbcConfiguration.create(url, info).driverPropertyInfo();
}
@Override

View File

@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class JdbcDataSource implements DataSource, Wrapper, Closeable {
private String url;
@ -93,7 +94,7 @@ public class JdbcDataSource implements DataSource, Wrapper, Closeable {
}
private Connection doGetConnection(Properties p) throws SQLException {
JdbcConfiguration cfg = new JdbcConfiguration(url, p);
JdbcConfiguration cfg = JdbcConfiguration.create(url, p);
if (loginTimeout > 0) {
cfg.connectTimeout(TimeUnit.SECONDS.toMillis(loginTimeout));
}

View File

@ -15,7 +15,7 @@ import static org.hamcrest.Matchers.is;
public class JdbcConfigurationTests extends ESTestCase {
private JdbcConfiguration ci(String url) throws SQLException {
return new JdbcConfiguration(url, null);
return JdbcConfiguration.create(url, null);
}
public void testJustThePrefix() throws Exception {

View File

@ -5,9 +5,28 @@
*/
package org.elasticsearch.xpack.sql.net.client;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.elasticsearch.xpack.sql.net.client.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static java.util.Collections.emptyList;
/**
* Common configuration class used for client.
* Uses a Properties object to be created (as clients would use strings to configure it).
* While this is convenient, it makes validation tricky (of both the names and values) and thus
* it's available only during construction.
* Some values might be updated later on in a typed fashion (dedicated method) in order
* to move away from the loose Strings...
*/
public class ConnectionConfiguration {
public static class HostAndPort {
@ -55,9 +74,15 @@ public class ConnectionConfiguration {
public static final String AUTH_USER = "user";
public static final String AUTH_PASS = "pass";
// Proxy
protected static final Set<String> OPTION_NAMES = new LinkedHashSet<>(
Arrays.asList(CONNECT_TIMEOUT, NETWORK_TIMEOUT, QUERY_TIMEOUT, PAGE_TIMEOUT, PAGE_SIZE));
private final Properties settings;
static {
OPTION_NAMES.addAll(SslConfig.OPTION_NAMES);
OPTION_NAMES.addAll(ProxyConfig.OPTION_NAMES);
}
// Proxy
private long connectTimeout;
private long networkTimeout;
@ -68,19 +93,20 @@ public class ConnectionConfiguration {
private final String user, pass;
private final SslConfig sslConfig;
private final ProxyConfig proxyConfig;
public ConnectionConfiguration(Properties props) {
settings = props != null ? new Properties(props) : new Properties();
public ConnectionConfiguration(Properties props) throws ClientException {
Properties settings = props != null ? props : new Properties();
connectTimeout = Long.parseLong(settings.getProperty(CONNECT_TIMEOUT, CONNECT_TIMEOUT_DEFAULT));
networkTimeout = Long.parseLong(settings.getProperty(NETWORK_TIMEOUT, NETWORK_TIMEOUT_DEFAULT));
queryTimeout = Long.parseLong(settings.getProperty(QUERY_TIMEOUT, QUERY_TIMEOUT_DEFAULT));
checkPropertyNames(settings, optionNames());
connectTimeout = parseValue(CONNECT_TIMEOUT, settings.getProperty(CONNECT_TIMEOUT, CONNECT_TIMEOUT_DEFAULT), Long::parseLong);
networkTimeout = parseValue(NETWORK_TIMEOUT, settings.getProperty(NETWORK_TIMEOUT, NETWORK_TIMEOUT_DEFAULT), Long::parseLong);
queryTimeout = parseValue(QUERY_TIMEOUT, settings.getProperty(QUERY_TIMEOUT, QUERY_TIMEOUT_DEFAULT), Long::parseLong);
// page
pageTimeout = Long.parseLong(settings.getProperty(PAGE_TIMEOUT, PAGE_TIMEOUT_DEFAULT));
pageSize = Integer.parseInt(settings.getProperty(PAGE_SIZE, PAGE_SIZE_DEFAULT));
pageTimeout = parseValue(PAGE_TIMEOUT, settings.getProperty(PAGE_TIMEOUT, PAGE_TIMEOUT_DEFAULT), Long::parseLong);
pageSize = parseValue(PAGE_SIZE, settings.getProperty(PAGE_SIZE, PAGE_SIZE_DEFAULT), Integer::parseInt);
// auth
user = settings.getProperty(AUTH_USER);
@ -90,6 +116,42 @@ public class ConnectionConfiguration {
proxyConfig = new ProxyConfig(settings);
}
private Collection<String> optionNames() {
Collection<String> options = new ArrayList<>(OPTION_NAMES);
options.addAll(extraOptions());
return options;
}
protected Collection<? extends String> extraOptions() {
return emptyList();
}
private static void checkPropertyNames(Properties settings, Collection<String> knownNames) throws ClientException {
// validate specified properties to pick up typos and such
Enumeration<?> pNames = settings.propertyNames();
while (pNames.hasMoreElements()) {
String message = isKnownProperty(pNames.nextElement().toString(), knownNames);
if (message != null) {
throw new ClientException(message);
}
}
}
private static String isKnownProperty(String propertyName, Collection<String> knownOptions) {
if (knownOptions.contains(propertyName)) {
return null;
}
return "Unknown parameter [" + propertyName + "] ; did you mean " + StringUtils.findSimiliar(propertyName, knownOptions);
}
protected <T> T parseValue(String key, String value, Function<String, T> parser) {
try {
return parser.apply(value);
} catch (Exception ex) {
throw new ClientException("Cannot parse property [" + key + "] with value [" + value + "]; " + ex.getMessage());
}
}
protected boolean isSSLEnabled() {
return sslConfig.isEnabled();
}
@ -102,10 +164,6 @@ public class ConnectionConfiguration {
return proxyConfig;
}
protected Properties settings() {
return settings;
}
public void connectTimeout(long millis) {
connectTimeout = millis;
}
@ -139,7 +197,6 @@ public class ConnectionConfiguration {
}
// auth
public String authUser() {
return user;
}

View File

@ -11,7 +11,10 @@ import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;
class ProxyConfig {
@ -20,6 +23,8 @@ class ProxyConfig {
private static final String SOCKS_PROXY = "proxy.socks";
private static final String SOCKS_PROXY_DEFAULT = StringUtils.EMPTY;
static final Set<String> OPTION_NAMES = new LinkedHashSet<>(Arrays.asList(HTTP_PROXY, SOCKS_PROXY));
private final Proxy proxy;
ProxyConfig(Properties settings) {
@ -32,12 +37,7 @@ class ProxyConfig {
address = host(settings.getProperty(SOCKS_PROXY, SOCKS_PROXY_DEFAULT), 1080);
type = Proxy.Type.SOCKS;
}
if (address != null) {
proxy = createProxy(type, address);
}
else {
proxy = null;
}
proxy = address != null ? createProxy(type, address) : null;
}
@SuppressForbidden(reason = "create the actual proxy")

View File

@ -15,8 +15,11 @@ import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
@ -51,6 +54,10 @@ class SslConfig {
private static final String SSL_TRUSTSTORE_TYPE = "ssl.truststore.type";
private static final String SSL_TRUSTSTORE_TYPE_DEFAULT = "JKS";
static final Set<String> OPTION_NAMES = new LinkedHashSet<>(Arrays.asList(SSL, SSL_PROTOCOL,
SSL_KEYSTORE_LOCATION, SSL_KEYSTORE_PASS, SSL_KEYSTORE_TYPE,
SSL_TRUSTSTORE_LOCATION, SSL_TRUSTSTORE_PASS, SSL_TRUSTSTORE_TYPE));
private final boolean enabled;
private final String protocol, keystoreLocation, keystorePass, keystoreType;
private final String truststoreLocation, truststorePass, truststoreType;